import { createAppSettings } from "@shared/testing/factories.js";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { toast } from "sonner";
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "../api";
import { renderWithQueryClient } from "../test/renderWithQueryClient";
import { SettingsPage } from "./SettingsPage";

const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;

const render = (ui: Parameters<typeof renderWithQueryClient>[0]) =>
  renderWithQueryClient(ui);

vi.mock("../api", () => ({
  getSettings: vi.fn(),
  getLlmModels: vi.fn().mockResolvedValue([]),
  updateSettings: vi.fn(),
  clearDatabase: vi.fn(),
  deleteJobsByStatus: vi.fn(),
}));

vi.mock("sonner", () => ({
  toast: {
    success: vi.fn(),
    error: vi.fn(),
    info: vi.fn(),
  },
}));

const baseSettings = createAppSettings({
  profileProjects: [
    {
      id: "proj-1",
      name: "Project One",
      description: "Desc 1",
      date: "2024",
      isVisibleInBase: true,
    },
    {
      id: "proj-2",
      name: "Project Two",
      description: "Desc 2",
      date: "2023",
      isVisibleInBase: false,
    },
  ],
});

const renderPage = () => {
  return render(
    <MemoryRouter initialEntries={["/settings"]}>
      <SettingsPage />
    </MemoryRouter>,
  );
};

const openModelSection = async () => {
  const modelTrigger = await screen.findByRole("button", { name: /^model$/i });
  fireEvent.click(modelTrigger);
};

describe("SettingsPage", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
      configurable: true,
      value: vi.fn(),
    });
  });

  afterAll(() => {
    Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
      configurable: true,
      value: originalScrollIntoView,
    });
  });

  it("saves trimmed model overrides", async () => {
    vi.mocked(api.getSettings).mockResolvedValue(baseSettings);
    vi.mocked(api.updateSettings).mockResolvedValue({
      ...baseSettings,
      model: {
        value: "gpt-4",
        default: baseSettings.model.default,
        override: "gpt-4",
      },
    });

    renderPage();
    await openModelSection();

    const modelInput = screen.getByLabelText(/default model/i);
    await waitFor(() => expect(modelInput).toBeEnabled());
    fireEvent.change(modelInput, { target: { value: "  gpt-4  " } });

    const saveButton = screen.getByRole("button", { name: /^save$/i });
    await waitFor(() => expect(saveButton).toBeEnabled());

    fireEvent.click(saveButton);

    await waitFor(() => expect(api.updateSettings).toHaveBeenCalled());
    expect(api.updateSettings).toHaveBeenCalledWith(
      expect.objectContaining({
        model: "gpt-4",
      }),
    );
    expect(toast.success).toHaveBeenCalledWith("Settings saved");
  });

  it("shows validation error for too long model override", async () => {
    vi.mocked(api.getSettings).mockResolvedValue(baseSettings);

    renderPage();
    await openModelSection();

    const modelInput = screen.getByLabelText(/default model/i);
    await waitFor(() => expect(modelInput).toBeEnabled());

    // Change to > 200 chars
    fireEvent.change(modelInput, { target: { value: "a".repeat(201) } });

    // Should see error message
    expect(
      await screen.findByText(
        /String must contain at most 200 character\(s\)/i,
      ),
    ).toBeInTheDocument();

    // Save button should be disabled due to validation error (isValid will be false)
    const saveButton = screen.getByRole("button", { name: /^save$/i });
    expect(saveButton).toBeDisabled();
  });

  it("clears jobs by status and summarizes results", async () => {
    vi.mocked(api.getSettings).mockResolvedValue(baseSettings);
    vi.mocked(api.deleteJobsByStatus).mockResolvedValue({
      message: "",
      count: 2,
    });

    renderPage();

    const dangerTrigger = await screen.findByRole("button", {
      name: /danger zone/i,
    });
    fireEvent.click(dangerTrigger);

    const clearSelectedButton = await screen.findByRole("button", {
      name: /clear selected/i,
    });
    fireEvent.click(clearSelectedButton);

    const confirmButton = await screen.findByRole("button", {
      name: /clear 1 status/i,
    });
    fireEvent.click(confirmButton);

    await waitFor(() =>
      expect(api.deleteJobsByStatus).toHaveBeenCalledWith("discovered"),
    );
    expect(toast.success).toHaveBeenCalledWith(
      "Jobs cleared",
      expect.objectContaining({
        description: "Deleted 2 jobs: 2 discovered",
      }),
    );
  });

  it("enables save button when model is changed", async () => {
    vi.mocked(api.getSettings).mockResolvedValue(baseSettings);
    renderPage();
    const saveButton = screen.getByRole("button", { name: /^save$/i });
    expect(saveButton).toBeDisabled();
    await openModelSection();

    const modelInput = screen.getByLabelText(/default model/i);
    // Wait for the query to resolve and input to be enabled
    await waitFor(() => expect(modelInput).toBeEnabled());

    fireEvent.change(modelInput, { target: { value: "new-model" } });
    await waitFor(() => expect(saveButton).toBeEnabled());
  });

  it("clears stale model overrides when the provider changes", async () => {
    vi.mocked(api.getSettings).mockResolvedValue(
      createAppSettings({
        model: {
          value: "google/gemini-3-flash-preview",
          default: "google/gemini-3-flash-preview",
          override: "google/gemini-3-flash-preview",
        },
        modelScorer: { value: "google/gemini-3-flash-preview", override: null },
        modelTailoring: {
          value: "google/gemini-3-flash-preview",
          override: "google/gemini-3-flash-preview",
        },
        modelProjectSelection: {
          value: "google/gemini-3-flash-preview",
          override: null,
        },
        llmProvider: { value: "gemini", default: "gemini", override: "gemini" },
      }),
    );
    vi.mocked(api.updateSettings).mockResolvedValue(baseSettings);

    renderPage();
    await openModelSection();

    fireEvent.click(screen.getByRole("combobox", { name: /provider/i }));
    fireEvent.click(await screen.findByText("OpenAI"));

    const saveButton = screen.getByRole("button", { name: /^save$/i });
    await waitFor(() => expect(saveButton).toBeEnabled());
    fireEvent.click(saveButton);

    await waitFor(() => expect(api.updateSettings).toHaveBeenCalled());
    expect(api.updateSettings).toHaveBeenCalledWith(
      expect.objectContaining({
        llmProvider: "openai",
        model: null,
        modelScorer: null,
        modelTailoring: null,
        modelProjectSelection: null,
      }),
    );
  });

  it("hides pipeline tuning and unused settings sections", async () => {
    vi.mocked(api.getSettings).mockResolvedValue(baseSettings);
    renderPage();

    await screen.findByRole("button", { name: /model/i });
    expect(
      screen.queryByRole("button", { name: /ukvisajobs extractor/i }),
    ).not.toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: /gradcracker extractor/i }),
    ).not.toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: /search terms/i }),
    ).not.toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: /jobspy scraper/i }),
    ).not.toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: /display settings/i }),
    ).not.toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: /writing style & language/i }),
    ).not.toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: /backup/i }),
    ).not.toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: /tracer/i }),
    ).not.toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: /resume projects/i }),
    ).not.toBeInTheDocument();
  });

  it("enables save button when basic auth toggle is changed", async () => {
    vi.mocked(api.getSettings).mockResolvedValue(baseSettings);
    renderPage();
    const saveButton = screen.getByRole("button", { name: /^save$/i });

    const envTrigger = await screen.findByRole("button", {
      name: /environment & accounts/i,
    });
    fireEvent.click(envTrigger);
    const authCheckbox = screen.getByLabelText(/enable basic authentication/i);
    fireEvent.click(authCheckbox);
    expect(saveButton).toBeEnabled();
  });

  it("wipes basic auth credentials when toggle is disabled and saved", async () => {
    // Initial state: Basic Auth is active
    const activeSettings = {
      ...baseSettings,
      basicAuthActive: true,
      basicAuthUser: "admin",
      basicAuthPasswordHint: "pass",
    };
    vi.mocked(api.getSettings).mockResolvedValue(activeSettings);
    vi.mocked(api.updateSettings).mockResolvedValue(baseSettings);

    renderPage();

    const envTrigger = await screen.findByRole("button", {
      name: /environment & accounts/i,
    });
    fireEvent.click(envTrigger);

    const authCheckbox = screen.getByLabelText(/enable basic authentication/i);
    expect(authCheckbox).toBeChecked();

    // Disable it
    fireEvent.click(authCheckbox);
    expect(authCheckbox).not.toBeChecked();

    const saveButton = screen.getByRole("button", { name: /^save$/i });
    expect(saveButton).toBeEnabled();
    fireEvent.click(saveButton);

    await waitFor(() => expect(api.updateSettings).toHaveBeenCalled());
    expect(api.updateSettings).toHaveBeenCalledWith(
      expect.objectContaining({
        basicAuthUser: null,
        basicAuthPassword: null,
      }),
    );
  });

  it("saves blocked company keywords from scoring settings", async () => {
    vi.mocked(api.getSettings).mockResolvedValue(baseSettings);
    vi.mocked(api.updateSettings).mockResolvedValue({
      ...baseSettings,
      blockedCompanyKeywords: {
        value: ["staffing"],
        default: [],
        override: ["staffing"],
      },
    });

    renderPage();

    const scoringTrigger = await screen.findByRole("button", {
      name: /scoring settings/i,
    });
    fireEvent.click(scoringTrigger);

    const input = screen.getByPlaceholderText('e.g. "recruitment", "staffing"');
    fireEvent.change(input, { target: { value: "staffing" } });
    fireEvent.keyDown(input, { key: "Enter" });

    const saveButton = screen.getByRole("button", { name: /^save$/i });
    await waitFor(() => expect(saveButton).toBeEnabled());
    fireEvent.click(saveButton);

    await waitFor(() => expect(api.updateSettings).toHaveBeenCalled());
    expect(api.updateSettings).toHaveBeenCalledWith(
      expect.objectContaining({
        blockedCompanyKeywords: ["staffing"],
      }),
    );
  });

  it("saves scoring instructions from scoring settings", async () => {
    vi.mocked(api.getSettings).mockResolvedValue(baseSettings);
    vi.mocked(api.updateSettings).mockResolvedValue({
      ...baseSettings,
      scoringInstructions: {
        value:
          "Open to relocating, so do not mark down for location discrepancies.",
        default: "",
        override:
          "Open to relocating, so do not mark down for location discrepancies.",
      },
    });

    renderPage();

    const scoringTrigger = await screen.findByRole("button", {
      name: /scoring settings/i,
    });
    fireEvent.click(scoringTrigger);

    const textarea = screen.getByLabelText(/scoring instructions/i);
    fireEvent.change(textarea, {
      target: {
        value:
          "Open to relocating, so do not mark down for location discrepancies.",
      },
    });

    const saveButton = screen.getByRole("button", { name: /^save$/i });
    await waitFor(() => expect(saveButton).toBeEnabled());
    fireEvent.click(saveButton);

    await waitFor(() => expect(api.updateSettings).toHaveBeenCalled());
    expect(api.updateSettings).toHaveBeenCalledWith(
      expect.objectContaining({
        scoringInstructions:
          "Open to relocating, so do not mark down for location discrepancies.",
      }),
    );
  });
});
