/**
 * Tests for the "Add Log Path" form inside JailAccordionPanel (ConfigPage).
 *
 * Verifies that:
 *  - The add-log-path input and button are rendered inside the jail accordion.
 *  - The Add button is disabled when the input is empty.
 *  - Submitting a valid path calls `addLogPath` and appends the path to the list.
 *  - An API error is surfaced as an error message bar.
 */

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 { ConfigPage } from "../../pages/ConfigPage";
import type { JailConfig } from "../../types/config";

// ---------------------------------------------------------------------------
// Module mocks — use vi.hoisted so refs are available when vi.mock runs
// ---------------------------------------------------------------------------

const {
  mockAddLogPath,
  mockDeleteLogPath,
  mockUpdateJailConfig,
  mockReloadConfig,
  mockFetchGlobalConfig,
  mockFetchServerSettings,
  mockFetchJailConfigs,
  mockFetchMapColorThresholds,
  mockFetchJailConfigFiles,
  mockFetchFilterFiles,
  mockFetchActionFiles,
  mockUpdateMapColorThresholds,
  mockUpdateGlobalConfig,
  mockUpdateServerSettings,
  mockFlushLogs,
  mockSetJailConfigFileEnabled,
  mockUpdateJailConfigFile,
} = vi.hoisted(() => ({
  mockAddLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
  mockDeleteLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
  mockUpdateJailConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
  mockReloadConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
  mockFetchGlobalConfig: vi.fn().mockResolvedValue({
    config: {
      ban_time: 600,
      max_retry: 5,
      find_time: 300,
      backend: "auto",
    },
  }),
  mockFetchServerSettings: vi.fn().mockResolvedValue({
    settings: {
      log_level: "INFO",
      log_target: "STDOUT",
      syslog_socket: null,
      db_path: "/var/lib/fail2ban/fail2ban.sqlite3",
      db_purge_age: 86400,
      db_max_matches: 10,
    },
  }),
  mockFetchJailConfigs: vi.fn(),
  mockFetchMapColorThresholds: vi.fn().mockResolvedValue({
    threshold_high: 100,
    threshold_medium: 50,
    threshold_low: 20,
  }),
  mockFetchJailConfigFiles: vi.fn().mockResolvedValue({ files: [] }),
  mockFetchFilterFiles: vi.fn().mockResolvedValue({ files: [] }),
  mockFetchActionFiles: vi.fn().mockResolvedValue({ files: [] }),
  mockUpdateMapColorThresholds: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
  mockUpdateGlobalConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
  mockUpdateServerSettings: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
  mockFlushLogs: vi.fn().mockResolvedValue({ message: "ok" }),
  mockSetJailConfigFileEnabled: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
  mockUpdateJailConfigFile: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
}));

vi.mock("../../api/config", () => ({
  addLogPath: mockAddLogPath,
  deleteLogPath: mockDeleteLogPath,
  fetchJailConfigs: mockFetchJailConfigs,
  fetchJailConfig: vi.fn(),
  updateJailConfig: mockUpdateJailConfig,
  reloadConfig: mockReloadConfig,
  fetchGlobalConfig: mockFetchGlobalConfig,
  updateGlobalConfig: mockUpdateGlobalConfig,
  fetchServerSettings: mockFetchServerSettings,
  updateServerSettings: mockUpdateServerSettings,
  flushLogs: mockFlushLogs,
  fetchMapColorThresholds: mockFetchMapColorThresholds,
  updateMapColorThresholds: mockUpdateMapColorThresholds,
  fetchJailConfigFiles: mockFetchJailConfigFiles,
  fetchJailConfigFileContent: vi.fn(),
  updateJailConfigFile: mockUpdateJailConfigFile,
  setJailConfigFileEnabled: mockSetJailConfigFileEnabled,
  fetchFilterFiles: mockFetchFilterFiles,
  fetchFilterFile: vi.fn(),
  updateFilterFile: vi.fn(),
  createFilterFile: vi.fn(),
  fetchFilters: vi.fn().mockResolvedValue({ filters: [], total: 0 }),
  fetchFilter: vi.fn(),
  fetchActionFiles: mockFetchActionFiles,
  fetchActionFile: vi.fn(),
  updateActionFile: vi.fn(),
  createActionFile: vi.fn(),
  previewLog: vi.fn(),
  testRegex: vi.fn(),
  fetchInactiveJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
  activateJail: vi.fn(),
  deactivateJail: vi.fn(),
  fetchParsedFilter: vi.fn(),
  updateParsedFilter: vi.fn(),
  fetchParsedAction: vi.fn(),
  updateParsedAction: vi.fn(),
  fetchParsedJailFile: vi.fn(),
  updateParsedJailFile: vi.fn(),
}));

vi.mock("../../api/file_config", () => ({
  fetchJailConfigFiles: mockFetchJailConfigFiles,
  createJailConfigFile: vi.fn().mockResolvedValue({
    name: "new-jail",
    filename: "new-jail.conf",
    content: "# new-jail\n",
  }),
  fetchJailConfigFileContent: vi.fn(),
  updateJailConfigFile: mockUpdateJailConfigFile,
  setJailConfigFileEnabled: mockSetJailConfigFileEnabled,
  fetchFilterFiles: mockFetchFilterFiles,
  fetchFilterFile: vi.fn(),
  updateFilterFile: vi.fn(),
  createFilterFile: vi.fn(),
  fetchActionFiles: mockFetchActionFiles,
  fetchActionFile: vi.fn(),
  updateActionFile: vi.fn(),
  createActionFile: vi.fn(),
}));

vi.mock("../../api/jails", () => ({
  fetchJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
}));

/** Minimal jail fixture used across tests. */
const MOCK_JAIL: JailConfig = {
  name: "sshd",
  ban_time: 600,
  max_retry: 3,
  find_time: 300,
  fail_regex: [],
  ignore_regex: [],
  log_paths: ["/var/log/auth.log"],
  date_pattern: null,
  log_encoding: "UTF-8",
  backend: "auto",
  use_dns: "warn",
  prefregex: "",
  actions: [],
  bantime_escalation: null,
};

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

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

/** Waits for the sshd list item to appear and clicks it to open the detail pane. */
async function openSshdAccordion(user: ReturnType<typeof userEvent.setup>) {
  const listItem = await screen.findByRole("option", { name: /sshd/i });
  await user.click(listItem);
}

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

describe("ConfigPage — Add Log Path", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockFetchJailConfigs.mockResolvedValue({ jails: [MOCK_JAIL], total: 1 });
    mockAddLogPath.mockResolvedValue(undefined);
  });

  it("renders the existing log path and the add-log-path input inside the accordion", async () => {
    const user = userEvent.setup();
    renderConfigPage();
    await openSshdAccordion(user);

    // Existing path from fixture — rendered as an <input> value
    expect(screen.getByDisplayValue("/var/log/auth.log")).toBeInTheDocument();

    // Add-log-path input placeholder
    expect(
      screen.getByPlaceholderText("/var/log/example.log"),
    ).toBeInTheDocument();
  });

  it("disables the Add button when the path input is empty", async () => {
    const user = userEvent.setup();
    renderConfigPage();
    await openSshdAccordion(user);

    const addBtn = screen.getByRole("button", { name: /add log path/i });
    expect(addBtn).toBeDisabled();
  });

  it("enables the Add button when the path input has content", async () => {
    const user = userEvent.setup();
    renderConfigPage();
    await openSshdAccordion(user);

    const input = screen.getByPlaceholderText("/var/log/example.log");
    await user.type(input, "/var/log/nginx/access.log");

    const addBtn = screen.getByRole("button", { name: /add log path/i });
    expect(addBtn).not.toBeDisabled();
  });

  it("calls addLogPath and appends the path on successful submission", async () => {
    const user = userEvent.setup();
    renderConfigPage();
    await openSshdAccordion(user);

    const input = screen.getByPlaceholderText("/var/log/example.log");
    await user.type(input, "/var/log/nginx/access.log");

    const addBtn = screen.getByRole("button", { name: /add log path/i });
    await user.click(addBtn);

    await waitFor(() => {
      expect(mockAddLogPath).toHaveBeenCalledWith("sshd", {
        log_path: "/var/log/nginx/access.log",
        tail: true,
      });
    });

    // New path should appear in the list as an <input> value
    expect(screen.getByDisplayValue("/var/log/nginx/access.log")).toBeInTheDocument();

    // Input should be cleared
    expect(input).toHaveValue("");
  });

  it("shows a success message after adding a log path", async () => {
    const user = userEvent.setup();
    renderConfigPage();
    await openSshdAccordion(user);

    const input = screen.getByPlaceholderText("/var/log/example.log");
    await user.type(input, "/var/log/nginx/access.log");
    await user.click(screen.getByRole("button", { name: /add log path/i }));

    await waitFor(() => {
      expect(
        screen.getByText(/added log path.*\/var\/log\/nginx\/access\.log/i),
      ).toBeInTheDocument();
    });
  });

  it("shows an error message when addLogPath fails", async () => {
    mockAddLogPath.mockRejectedValueOnce(new Error("Connection refused"));
    const user = userEvent.setup();
    renderConfigPage();
    await openSshdAccordion(user);

    const input = screen.getByPlaceholderText("/var/log/example.log");
    await user.type(input, "/var/log/bad.log");
    await user.click(screen.getByRole("button", { name: /add log path/i }));

    await waitFor(() => {
      expect(
        screen.getByText("Failed to add log path."),
      ).toBeInTheDocument();
    });
  });
});
