/**
 * Tests for the LogTab component (Task 2).
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import { LogTab } from "../LogTab";
import type { Fail2BanLogResponse, ServiceStatusResponse } from "../../../types/config";

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------

vi.mock("../../../api/config", () => ({
  fetchFail2BanLog: vi.fn(),
  fetchServiceStatus: vi.fn(),
}));

import { fetchFail2BanLog, fetchServiceStatus } from "../../../api/config";

const mockFetchLog = vi.mocked(fetchFail2BanLog);
const mockFetchStatus = vi.mocked(fetchServiceStatus);

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

const onlineStatus: ServiceStatusResponse = {
  online: true,
  version: "1.0.2",
  jail_count: 3,
  total_bans: 12,
  total_failures: 5,
  log_level: "INFO",
  log_target: "/var/log/fail2ban.log",
};

const offlineStatus: ServiceStatusResponse = {
  online: false,
  version: null,
  jail_count: 0,
  total_bans: 0,
  total_failures: 0,
  log_level: "UNKNOWN",
  log_target: "UNKNOWN",
};

const logResponse: Fail2BanLogResponse = {
  log_path: "/var/log/fail2ban.log",
  lines: [
    "2025-01-01 12:00:00 INFO  sshd Found 1.2.3.4",
    "2025-01-01 12:00:01 WARNING sshd Too many failures",
    "2025-01-01 12:00:02 ERROR  fail2ban something went wrong",
  ],
  total_lines: 1000,
  log_level: "INFO",
  log_target: "/var/log/fail2ban.log",
};

const nonFileLogResponse: Fail2BanLogResponse = {
  ...logResponse,
  log_target: "STDOUT",
  lines: [],
};

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

function renderTab() {
  return render(
    <FluentProvider theme={webLightTheme}>
      <LogTab />
    </FluentProvider>,
  );
}

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

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

  it("shows a spinner while loading", () => {
    // Never resolves during this test.
    mockFetchStatus.mockReturnValue(new Promise(() => undefined));
    mockFetchLog.mockReturnValue(new Promise(() => undefined));

    renderTab();

    expect(screen.getByText(/loading log viewer/i)).toBeInTheDocument();
  });

  it("renders the health panel with Running badge when online", async () => {
    mockFetchStatus.mockResolvedValue(onlineStatus);
    mockFetchLog.mockResolvedValue(logResponse);

    renderTab();

    await waitFor(() => { expect(screen.queryByText(/loading log viewer/i)).toBeNull(); });

    expect(screen.getByText("Running")).toBeInTheDocument();
    expect(screen.getByText("1.0.2")).toBeInTheDocument();
    expect(screen.getByText("3")).toBeInTheDocument(); // active jails
    expect(screen.getByText("12")).toBeInTheDocument(); // total bans
  });

  it("renders the Offline badge and warning when fail2ban is down", async () => {
    mockFetchStatus.mockResolvedValue(offlineStatus);
    mockFetchLog.mockRejectedValue(new Error("not running"));

    renderTab();

    await waitFor(() => { expect(screen.queryByText(/loading log viewer/i)).toBeNull(); });

    expect(screen.getByText("Offline")).toBeInTheDocument();
    expect(screen.getByText(/not running or unreachable/i)).toBeInTheDocument();
  });

  it("renders log lines in the log viewer", async () => {
    mockFetchStatus.mockResolvedValue(onlineStatus);
    mockFetchLog.mockResolvedValue(logResponse);

    renderTab();

    await waitFor(() => {
      expect(screen.getByText(/2025-01-01 12:00:00 INFO/)).toBeInTheDocument();
    });

    expect(screen.getByText(/2025-01-01 12:00:01 WARNING/)).toBeInTheDocument();
    expect(screen.getByText(/2025-01-01 12:00:02 ERROR/)).toBeInTheDocument();
  });

  it("shows a non-file target info banner when log_target is STDOUT", async () => {
    mockFetchStatus.mockResolvedValue(onlineStatus);
    mockFetchLog.mockResolvedValue(nonFileLogResponse);

    renderTab();

    await waitFor(() => {
      expect(screen.getByText(/fail2ban is logging to/i)).toBeInTheDocument();
    });

    expect(screen.getByText(/STDOUT/)).toBeInTheDocument();
    expect(screen.queryByText(/Refresh/)).toBeNull();
  });

  it("shows empty state when no lines match the filter", async () => {
    mockFetchStatus.mockResolvedValue(onlineStatus);
    mockFetchLog.mockResolvedValue({ ...logResponse, lines: [] });

    renderTab();

    await waitFor(() => {
      expect(screen.getByText(/no log entries found/i)).toBeInTheDocument();
    });
  });

  it("shows truncation notice when total_lines > lines.length", async () => {
    mockFetchStatus.mockResolvedValue(onlineStatus);
    mockFetchLog.mockResolvedValue({ ...logResponse, lines: logResponse.lines, total_lines: 1000 });

    renderTab();

    await waitFor(() => {
      expect(screen.getByText(/showing last/i)).toBeInTheDocument();
    });
  });

  it("calls fetchFail2BanLog again on Refresh button click", async () => {
    mockFetchStatus.mockResolvedValue(onlineStatus);
    mockFetchLog.mockResolvedValue(logResponse);

    const user = userEvent.setup();
    renderTab();

    await waitFor(() => { expect(screen.getByText(/Refresh/)).toBeInTheDocument(); });

    const refreshBtn = screen.getByRole("button", { name: /refresh/i });
    await user.click(refreshBtn);

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