/**
 * Tests for the `BannedIpsSection` component.
 *
 * Verifies:
 *  - Renders the section header and total count badge.
 *  - Shows a spinner while loading.
 *  - Renders a table with IP rows on success.
 *  - Shows an empty-state message when there are no banned IPs.
 *  - Displays an error message bar when the API call fails.
 *  - Search input re-fetches with the search parameter after debounce.
 *  - Unban button calls `unbanIp` and refreshes the list.
 *  - Pagination buttons are shown and change the page.
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor, act, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import { BannedIpsSection } from "../BannedIpsSection";
import type { JailBannedIpsResponse } from "../../../types/jail";

// ---------------------------------------------------------------------------
// Module mocks
// ---------------------------------------------------------------------------

const { mockFetchJailBannedIps, mockUnbanIp } = vi.hoisted(() => ({
  mockFetchJailBannedIps: vi.fn<
    (
      jailName: string,
      page?: number,
      pageSize?: number,
      search?: string,
    ) => Promise<JailBannedIpsResponse>
  >(),
  mockUnbanIp: vi.fn<
    (ip: string, jail?: string) => Promise<{ message: string; jail: string }>
  >(),
}));

vi.mock("../../../api/jails", () => ({
  fetchJailBannedIps: mockFetchJailBannedIps,
  unbanIp: mockUnbanIp,
}));

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

function makeBan(ip: string) {
  return {
    ip,
    jail: "sshd",
    banned_at: "2025-01-01T10:00:00+00:00",
    expires_at: "2025-01-01T10:10:00+00:00",
    ban_count: 1,
    country: "US",
  };
}

function makeResponse(
  ips: string[] = ["1.2.3.4", "5.6.7.8"],
  total = 2,
): JailBannedIpsResponse {
  return {
    items: ips.map(makeBan),
    total,
    page: 1,
    page_size: 25,
  };
}

const EMPTY_RESPONSE: JailBannedIpsResponse = {
  items: [],
  total: 0,
  page: 1,
  page_size: 25,
};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function renderSection(jailName = "sshd") {
  return render(
    <FluentProvider theme={webLightTheme}>
      <BannedIpsSection jailName={jailName} />
    </FluentProvider>,
  );
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("BannedIpsSection", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.useRealTimers();
    mockUnbanIp.mockResolvedValue({ message: "ok", jail: "sshd" });
  });

  it("renders section header with 'Currently Banned IPs' title", async () => {
    mockFetchJailBannedIps.mockResolvedValue(makeResponse());
    renderSection();
    await waitFor(() => {
      expect(screen.getByText("Currently Banned IPs")).toBeTruthy();
    });
  });

  it("shows the total count badge", async () => {
    mockFetchJailBannedIps.mockResolvedValue(makeResponse(["1.2.3.4", "5.6.7.8"], 2));
    renderSection();
    await waitFor(() => {
      expect(screen.getByText("2")).toBeTruthy();
    });
  });

  it("shows a spinner while loading", () => {
    // Never resolves during this test so we see the spinner.
    mockFetchJailBannedIps.mockReturnValue(new Promise(() => void 0));
    renderSection();
    expect(screen.getByText("Loading banned IPs…")).toBeTruthy();
  });

  it("renders IP rows when banned IPs exist", async () => {
    mockFetchJailBannedIps.mockResolvedValue(makeResponse(["1.2.3.4", "5.6.7.8"]));
    renderSection();
    await waitFor(() => {
      expect(screen.getByText("1.2.3.4")).toBeTruthy();
      expect(screen.getByText("5.6.7.8")).toBeTruthy();
    });
  });

  it("shows empty-state message when no IPs are banned", async () => {
    mockFetchJailBannedIps.mockResolvedValue(EMPTY_RESPONSE);
    renderSection();
    await waitFor(() => {
      expect(
        screen.getByText("No IPs currently banned in this jail."),
      ).toBeTruthy();
    });
  });

  it("shows an error message bar on API failure", async () => {
    mockFetchJailBannedIps.mockRejectedValue(new Error("socket dead"));
    renderSection();
    await waitFor(() => {
      expect(screen.getByText(/socket dead/i)).toBeTruthy();
    });
  });

  it("calls fetchJailBannedIps with the jail name", async () => {
    mockFetchJailBannedIps.mockResolvedValue(makeResponse());
    renderSection("nginx");
    await waitFor(() => {
      expect(mockFetchJailBannedIps).toHaveBeenCalledWith(
        "nginx",
        expect.any(Number),
        expect.any(Number),
        undefined,
      );
    });
  });

  it("search input re-fetches after debounce with the search term", async () => {
    vi.useFakeTimers();
    mockFetchJailBannedIps.mockResolvedValue(makeResponse());
    renderSection();
    // Flush pending async work from the initial render (no timer advancement needed).
    await act(async () => {});

    mockFetchJailBannedIps.mockClear();
    mockFetchJailBannedIps.mockResolvedValue(
      makeResponse(["1.2.3.4"], 1),
    );

    // fireEvent is synchronous — avoids hanging with fake timers.
    const input = screen.getByPlaceholderText("e.g. 192.168");
    act(() => {
      fireEvent.change(input, { target: { value: "1.2.3" } });
    });

    // Advance just past the 300ms debounce delay and flush promises.
    await act(async () => {
      await vi.advanceTimersByTimeAsync(350);
    });

    expect(mockFetchJailBannedIps).toHaveBeenLastCalledWith(
      "sshd",
      expect.any(Number),
      expect.any(Number),
      "1.2.3",
    );

    vi.useRealTimers();
  });

  it("calls unbanIp when the unban button is clicked", async () => {
    mockFetchJailBannedIps.mockResolvedValue(makeResponse(["1.2.3.4"]));
    renderSection();
    await waitFor(() => {
      expect(screen.getByText("1.2.3.4")).toBeTruthy();
    });

    const unbanBtn = screen.getByLabelText("Unban 1.2.3.4");
    await userEvent.click(unbanBtn);

    expect(mockUnbanIp).toHaveBeenCalledWith("1.2.3.4", "sshd");
  });

  it("refreshes list after successful unban", async () => {
    mockFetchJailBannedIps
      .mockResolvedValueOnce(makeResponse(["1.2.3.4"]))
      .mockResolvedValue(EMPTY_RESPONSE);
    mockUnbanIp.mockResolvedValue({ message: "ok", jail: "sshd" });

    renderSection();
    await waitFor(() => {
      expect(screen.getByText("1.2.3.4")).toBeTruthy();
    });

    const unbanBtn = screen.getByLabelText("Unban 1.2.3.4");
    await userEvent.click(unbanBtn);

    await waitFor(() => {
      expect(mockFetchJailBannedIps).toHaveBeenCalledTimes(2);
    });
  });

  it("shows pagination controls when total > 0", async () => {
    mockFetchJailBannedIps.mockResolvedValue(
      makeResponse(["1.2.3.4", "5.6.7.8"], 50),
    );
    renderSection();
    await waitFor(() => {
      expect(screen.getByLabelText("Next page")).toBeTruthy();
      expect(screen.getByLabelText("Previous page")).toBeTruthy();
    });
  });

  it("previous page button is disabled on page 1", async () => {
    mockFetchJailBannedIps.mockResolvedValue(
      makeResponse(["1.2.3.4"], 50),
    );
    renderSection();
    await waitFor(() => {
      const prevBtn = screen.getByLabelText("Previous page");
      expect(prevBtn).toHaveAttribute("disabled");
    });
  });
});
