import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useSetup } from "../useSetup";
import { useServerHealth } from "../useServerHealth";
import * as setupApi from "../../api/setup";
import * as configApi from "../../api/config";
import type { Fail2BanLogResponse, ServiceStatusResponse } from "../../types/config";
import * as sharedSetupModule from "../useSharedSetupStatus";

vi.mock("../../api/setup");
vi.mock("../../api/config");
vi.mock("../useSharedSetupStatus", () => ({
  useSharedSetupStatus: vi.fn(),
  invalidateSetupStatus: vi.fn(),
}));

const mockedUseSharedSetupStatus = vi.mocked(sharedSetupModule.useSharedSetupStatus);
const mockedFetchServiceStatus = vi.mocked(configApi.fetchServiceStatus);
const mockedFetchFail2BanLog = vi.mocked(configApi.fetchFail2BanLog);

describe("useSetup", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("loads setup status on mount via the shared hook", async () => {
    mockedUseSharedSetupStatus.mockReturnValue({
      status: { completed: true },
      loading: false,
      error: null,
      refresh: vi.fn(),
    });

    const { result } = renderHook(() => useSetup());

    await act(async () => {
      await Promise.resolve();
    });

    expect(result.current.status).toEqual({ completed: true });
    expect(result.current.loading).toBe(false);
    expect(result.current.error).toBeNull();
  });

  it("calls invalidateSetupStatus after successful setup submission", async (): Promise<void> => {
    const invalidateSpy = vi.spyOn(sharedSetupModule, "invalidateSetupStatus");
    mockedUseSharedSetupStatus.mockReturnValue({
      status: { completed: false },
      loading: false,
      error: null,
      refresh: vi.fn(),
    });

    const mockSubmitSetup = vi.mocked(setupApi.submitSetup);
    mockSubmitSetup.mockResolvedValue({ message: "Setup successful" });

    const { result } = renderHook(() => useSetup());

    await act(async () => {
      await result.current.submit({
        master_password: "TestPass123!",
      });
    });

    expect(invalidateSpy).toHaveBeenCalledTimes(1);
  });

  it("handles shared hook loading and error states", async () => {
    mockedUseSharedSetupStatus.mockReturnValue({
      status: null,
      loading: true,
      error: null,
      refresh: vi.fn(),
    });

    const { result } = renderHook(() => useSetup());

    expect(result.current.status).toBeNull();
    expect(result.current.loading).toBe(true);
    expect(result.current.error).toBeNull();
  });

  it("propagates refresh from shared hook", (): void => {
    const refreshMock = vi.fn();
    mockedUseSharedSetupStatus.mockReturnValue({
      status: { completed: true },
      loading: false,
      error: null,
      refresh: refreshMock,
    });

    const { result } = renderHook(() => useSetup());

    expect(result.current.refresh).toBe(refreshMock);
  });
});

describe("useServerHealth", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("updates state only for the latest refresh and aborts stale requests", async () => {
    const firstService = { running: false } as unknown as ServiceStatusResponse;
    const secondService = { running: true } as unknown as ServiceStatusResponse;
    const firstLog = { lines: [] } as unknown as Fail2BanLogResponse;
    const secondLog = { lines: ["ok"] } as unknown as Fail2BanLogResponse;
    let callCount = 0;

    mockedFetchServiceStatus.mockImplementation(async (signal?: AbortSignal) => {
      const response = callCount++ === 0 ? firstService : secondService;
      await new Promise((resolve) => setTimeout(resolve, 10));
      if (signal?.aborted) {
        throw new Error("Request aborted");
      }
      return response;
    });
    mockedFetchFail2BanLog.mockImplementation(async (_lines?: number, _filter?: string, signal?: AbortSignal) => {
      const response = callCount === 1 ? firstLog : secondLog;
      await new Promise((resolve) => setTimeout(resolve, 10));
      if (signal?.aborted) {
        throw new Error("Request aborted");
      }
      return response;
    });

    const { result } = renderHook(() => useServerHealth(10, "test"));

    await act(async () => {
      void result.current.refresh();
      void result.current.refresh();
      vi.advanceTimersByTime(20);
      await Promise.resolve();
    });

    expect(result.current.status).toEqual(secondService);
    expect(result.current.logData).toEqual(secondLog);
    expect(result.current.error).toBeNull();
    expect(mockedFetchServiceStatus).toHaveBeenCalledTimes(2);
    expect(mockedFetchFail2BanLog).toHaveBeenCalledTimes(2);
  });
});
