import React from "react";
import "@testing-library/jest-dom";
import { fireEvent, render as rtlRender, screen, waitFor } from "@testing-library/react";

import { ApplicationFollowUp, ApplicationInterviewPrep } from "./components/InterviewPrep";
import { api } from "./api";
import { I18nProvider } from "./i18n/I18nProvider";
import { ConfirmProvider } from "./confirm";

const render = (ui: React.ReactElement) => rtlRender(<I18nProvider><ConfirmProvider>{ui}</ConfirmProvider></I18nProvider>);

jest.mock("./api", () => ({
  api: {
    get: jest.fn(),
    post: jest.fn(),
    patch: jest.fn(),
    put: jest.fn(),
    delete: jest.fn(),
    interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
  },
  getApiErrorMessage: (_e: any, fallback?: string) => fallback || "Request failed.",
}));

const mockedApi = api as jest.Mocked<typeof api>;

const board = {
  groups: [
    {
      category: "company-research",
      label: "Company research",
      items: [
        { id: 1, category: "company-research", title: "Funding history", content: "Series B in 2025.", source: "user", isPrepared: true, sortOrder: 1, updatedAtUtc: "2026-07-19T10:00:00Z" },
      ],
    },
    {
      category: "behavioural",
      label: "Behavioural questions",
      items: [
        { id: 2, category: "behavioural", title: "Tell me about a conflict", content: null, source: "ai", isPrepared: false, sortOrder: 2, updatedAtUtc: "2026-07-19T10:00:00Z" },
      ],
    },
  ],
  total: 2,
  prepared: 1,
  percent: 50,
  isInterviewStage: true,
  aiSuggestionCount: 1,
};

const followUp = { followUpAt: "2026-07-26T00:00:00", nextAction: "Chase recruiter", responseReceived: false, openFollowUpTasks: 1 };

function routeGet(overrides: Record<string, any> = {}) {
  mockedApi.get.mockImplementation((url: string) => {
    if (url.endsWith("/follow-up")) return Promise.resolve({ data: overrides.followUp ?? followUp } as any);
    if (url.endsWith("/cv")) return Promise.resolve({ data: overrides.cv ?? { attachedVariantId: 3, attachedVariantName: "Backend CV" } } as any);
    return Promise.resolve({ data: overrides.board ?? board } as any);
  });
}

beforeEach(() => jest.clearAllMocks());

test("prep renders grouped items with progress and marks AI-sourced ones", async () => {
  routeGet();

  render(<ApplicationInterviewPrep jobId={7} />);

  // Each label appears twice: once as the group heading, once as a category option in the add form.
  expect((await screen.findAllByText("Company research")).length).toBeGreaterThan(0);
  expect(screen.getAllByText("Behavioural questions").length).toBeGreaterThan(0);
  expect(screen.getByText("Funding history")).toBeInTheDocument();
  expect(screen.getByText("1 of 2 ready")).toBeInTheDocument();
  expect(screen.getByText("From AI")).toBeInTheDocument();
});

test("adding a prep item posts the chosen category and title", async () => {
  routeGet();
  mockedApi.post.mockResolvedValue({ data: board.groups[0].items[0] } as any);

  render(<ApplicationInterviewPrep jobId={7} />);
  fireEvent.change(await screen.findByLabelText(/Add a question, topic or note/i), {
    target: { value: "What does success look like?" },
  });
  fireEvent.click(screen.getByRole("button", { name: "Add" }));

  await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(
    "/jobapplications/7/interview-prep",
    { category: "company-research", title: "What does success look like?" },
  ));
});

test("marking an item ready patches it", async () => {
  routeGet();
  mockedApi.patch.mockResolvedValue({ data: board.groups[1].items[0] } as any);

  render(<ApplicationInterviewPrep jobId={7} />);
  fireEvent.click(await screen.findByRole("checkbox", { name: "Ready: Tell me about a conflict" }));

  await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith(
    "/jobapplications/7/interview-prep/2", { isPrepared: true }));
});

test("an answer is only saved when the user asks", async () => {
  routeGet();
  mockedApi.patch.mockResolvedValue({ data: board.groups[1].items[0] } as any);

  render(<ApplicationInterviewPrep jobId={7} />);
  const boxes = await screen.findAllByPlaceholderText(/Your answer, in your own words/i);
  fireEvent.change(boxes[1], { target: { value: "My STAR answer" } });

  // Typing alone must not persist anything.
  expect(mockedApi.patch).not.toHaveBeenCalled();

  fireEvent.click(screen.getByRole("button", { name: /Save answer/i }));
  await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith(
    "/jobapplications/7/interview-prep/2", { content: "My STAR answer" }));
});

test("deleting a prep item calls delete", async () => {
  routeGet();
  mockedApi.delete.mockResolvedValue({ data: undefined } as any);

  render(<ApplicationInterviewPrep jobId={7} />);
  fireEvent.click(await screen.findByRole("button", { name: "Delete: Funding history" }));
  fireEvent.click(await screen.findByRole("button", { name: "Delete" }));

  await waitFor(() => expect(mockedApi.delete).toHaveBeenCalledWith("/jobapplications/7/interview-prep/1"));
});

test("empty prep shows a useful empty state", async () => {
  routeGet({ board: { groups: [], total: 0, prepared: 0, percent: 0, isInterviewStage: false, aiSuggestionCount: 0 } });

  render(<ApplicationInterviewPrep jobId={7} />);

  expect(await screen.findByText(/Nothing prepared yet/i)).toBeInTheDocument();
  expect(screen.getByText(/has not reached an interview stage/i)).toBeInTheDocument();
});

test("AI preparation uses the linked CV and only saves after explicit acceptance", async () => {
  routeGet();
  mockedApi.post.mockImplementation((url: string) => {
    if (url.endsWith("/ai/generate")) {
      return Promise.resolve({ data: { result: { text: "## Technical\nExplain dependency injection." } } } as any);
    }
    return Promise.resolve({ data: board.groups[0].items[0] } as any);
  });

  render(<ApplicationInterviewPrep jobId={7} />);
  expect(await screen.findByText("Backend CV")).toBeInTheDocument();
  fireEvent.click(screen.getByRole("button", { name: "Generate preparation" }));

  expect(await screen.findByText(/Explain dependency injection/i)).toBeInTheDocument();
  expect(mockedApi.post).toHaveBeenCalledWith(
    "/jobapplications/7/ai/generate",
    expect.objectContaining({ module: "interview", targetLanguage: "en" }),
  );
  expect(mockedApi.post).not.toHaveBeenCalledWith(
    "/jobapplications/7/interview-prep",
    expect.anything(),
  );

  fireEvent.click(screen.getByRole("button", { name: "Add to preparation" }));
  await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(
    "/jobapplications/7/interview-prep",
    expect.objectContaining({ source: "ai", content: expect.stringContaining("dependency injection") }),
  ));
});

test("prep surfaces a load error", async () => {
  mockedApi.get.mockRejectedValue(new Error("boom"));

  render(<ApplicationInterviewPrep jobId={7} />);

  expect(await screen.findByText(/Could not load interview preparation/i)).toBeInTheDocument();
});

// ---------- follow-up ----------

test("follow-up loads the existing date and open checklist tasks", async () => {
  routeGet();

  render(<ApplicationFollowUp jobId={7} />);

  expect(await screen.findByDisplayValue("2026-07-26")).toBeInTheDocument();
  expect(screen.getByDisplayValue("Chase recruiter")).toBeInTheDocument();
  expect(screen.getByText(/1 open follow-up task on the checklist/i)).toBeInTheDocument();
});

test("saving a follow-up sends the date and next action", async () => {
  routeGet();
  mockedApi.put.mockResolvedValue({ data: followUp } as any);

  render(<ApplicationFollowUp jobId={7} />);
  fireEvent.change(await screen.findByLabelText(/Follow up on/i), { target: { value: "2026-08-01" } });
  fireEvent.click(screen.getByRole("button", { name: "Save" }));

  await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith(
    "/jobapplications/7/follow-up",
    { followUpAt: "2026-08-01", nextAction: "Chase recruiter" },
  ));
});

test("no follow-up shows an empty state", async () => {
  routeGet({ followUp: { followUpAt: null, nextAction: null, responseReceived: false, openFollowUpTasks: 0 } });

  render(<ApplicationFollowUp jobId={7} />);

  expect(await screen.findByText(/No follow-up scheduled/i)).toBeInTheDocument();
});
