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

import {
  ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection,
} from "./components/ApplicationAssets";
import { api } from "./api";
import { I18nProvider } from "./i18n/I18nProvider";

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

jest.mock("./api", () => ({
  api: {
    get: jest.fn(),
    put: jest.fn(),
    post: 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 cv = {
  attachedVariantId: 3,
  attachedVariantName: "Backend CV",
  attachedThemeId: "nordic",
  attachedVersion: 4,
  attachedUpdatedAtUtc: "2026-07-19T10:00:00Z",
  attachedIsPublic: false,
  hasTailoredCvText: false,
  availableVariants: [
    { id: 3, name: "Backend CV", themeId: "nordic", publicSlug: "abc", isPublic: false, version: 4, jobApplicationId: 7, updatedAtUtc: "2026-07-19T10:00:00Z" },
    { id: 5, name: "Generalist CV", themeId: "modern", publicSlug: "def", isPublic: false, version: 2, jobApplicationId: null, updatedAtUtc: "2026-07-18T10:00:00Z" },
  ],
};

const tailoring = {
  hasJobDescription: true,
  hasCareerProfile: true,
  hasAttachedVariant: true,
  matchScore: 72,
  suggestions: [
    { kind: "highlight-skills", title: "Skills to highlight", detail: "The advert asks for these.", items: ["C#", "SQL"] },
    { kind: "gaps", title: "Gaps to address", detail: null, items: ["Kubernetes"] },
  ],
  aiSuggestionCount: 0,
};

const coverLetter = {
  text: "Dear team",
  currentVersion: 2,
  versions: [
    { version: 2, source: "ai", aiAction: "improve", length: 9, createdAtUtc: "2026-07-19T10:00:00Z", isCurrent: true },
    { version: 1, source: "manual", aiAction: null, length: 40, createdAtUtc: "2026-07-19T09:00:00Z", isCurrent: false },
  ],
  aiSuggestionCount: 1,
};

function routeGet(overrides: Record<string, any> = {}) {
  mockedApi.get.mockImplementation((url: string) => {
    if (url.endsWith("/tailoring")) return Promise.resolve({ data: overrides.tailoring ?? tailoring } as any);
    if (url.endsWith("/cover-letter")) return Promise.resolve({ data: overrides.coverLetter ?? coverLetter } as any);
    return Promise.resolve({ data: overrides.cv ?? cv } as any);
  });
}

beforeEach(() => {
  window.localStorage.removeItem("uiLanguage");
  jest.clearAllMocks();
});

afterEach(() => window.localStorage.removeItem("uiLanguage"));

// ---------- CV ----------

test("cv section shows the attached variant and the ones available to attach", async () => {
  routeGet();

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

  expect(await screen.findByText("Backend CV")).toBeInTheDocument();
  expect(screen.getByText(/Theme nordic · version 4/)).toBeInTheDocument();
  // Must be the builder's real route (/career/builder/:id). An href the router does not serve
  // silently dead-ends the user, which is exactly what shipped before this test existed.
  expect(screen.getByRole("link", { name: /Edit, preview and export/i })).toHaveAttribute(
    "href", "/career/builder/3");
});

test("attaching a different variant only re-points the application", async () => {
  routeGet();
  mockedApi.put.mockResolvedValue({ data: { ...cv, attachedVariantId: 5, attachedVariantName: "Generalist CV" } } as any);

  render(<ApplicationCvSection jobId={7} />);
  fireEvent.mouseDown(await screen.findByRole("combobox", { name: /Attached CV variant/i }));
  fireEvent.click(await screen.findByRole("option", { name: /Generalist CV/ }));

  await waitFor(() =>
    expect(mockedApi.put).toHaveBeenCalledWith("/jobapplications/7/cv", { variantId: 5 }));
});

test("cv section points at the builder when there are no variants", async () => {
  routeGet({ cv: { ...cv, attachedVariantId: null, attachedVariantName: null, availableVariants: [] } });

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

  expect(await screen.findByText(/No CV variants yet/i)).toBeInTheDocument();
});

test("tailoring renders suggestions grouped by kind", async () => {
  routeGet();

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

  expect(await screen.findByText("Skills to highlight")).toBeInTheDocument();
  expect(screen.getByText("Gaps to address")).toBeInTheDocument();
  expect(screen.getByText("Kubernetes")).toBeInTheDocument();
});

test("tailoring asks for a career profile when there is none", async () => {
  routeGet({ tailoring: { ...tailoring, hasCareerProfile: false, suggestions: [] } });

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

  expect(await screen.findByText(/Build your career profile/i)).toBeInTheDocument();
});

// ---------- Cover letter ----------

test("cover letter loads the current text and its history", async () => {
  routeGet();

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

  expect(await screen.findByDisplayValue("Dear team")).toBeInTheDocument();
  expect(screen.getByText("v2")).toBeInTheDocument();
  expect(screen.getByText("ai · improve")).toBeInTheDocument();
  expect(screen.getByText("Current")).toBeInTheDocument();
});

test("editing marks the draft dirty and saving sends the new text", async () => {
  routeGet();
  mockedApi.put.mockResolvedValue({ data: { ...coverLetter, text: "Dear hiring team" } } as any);
  const onDirtyChange = jest.fn();

  render(<ApplicationCoverLetterSection jobId={7} onDirtyChange={onDirtyChange} />);
  fireEvent.change(await screen.findByLabelText("Cover letter"), { target: { value: "Dear hiring team" } });

  expect(screen.getByText("Unsaved changes")).toBeInTheDocument();
  expect(onDirtyChange).toHaveBeenLastCalledWith(true);
  fireEvent.click(screen.getByRole("button", { name: "Save" }));

  await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith(
    "/jobapplications/7/cover-letter",
    { text: "Dear hiring team", source: "manual", aiAction: undefined },
  ));
});

test("discarding returns to the saved text", async () => {
  routeGet();

  render(<ApplicationCoverLetterSection jobId={7} />);
  fireEvent.change(await screen.findByLabelText("Cover letter"), { target: { value: "scratch" } });
  fireEvent.click(screen.getByRole("button", { name: /Discard changes/i }));

  expect(await screen.findByDisplayValue("Dear team")).toBeInTheDocument();
  expect(mockedApi.put).not.toHaveBeenCalled();
});

test("restoring an old version calls the restore endpoint", async () => {
  routeGet();
  mockedApi.post.mockResolvedValue({ data: coverLetter } as any);

  render(<ApplicationCoverLetterSection jobId={7} />);
  fireEvent.click(await screen.findByRole("button", { name: /Restore version 1/i }));

  await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(
    "/jobapplications/7/cover-letter/versions/1/restore"));
});

test("an empty cover letter offers the template and an empty history", async () => {
  routeGet({ coverLetter: { text: null, currentVersion: 0, versions: [], aiSuggestionCount: 0 } });

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

  expect(await screen.findByText(/No versions yet/i)).toBeInTheDocument();
  fireEvent.click(screen.getByRole("button", { name: /Start from template/i }));
  expect((screen.getByLabelText("Cover letter") as HTMLTextAreaElement).value)
    .toContain("Dear Hiring Manager");
});

test("the empty cover-letter template and rich-text controls follow Bokmål UI language", async () => {
  window.localStorage.setItem("uiLanguage", "nb");
  routeGet({ coverLetter: { text: null, currentVersion: 0, versions: [], aiSuggestionCount: 0 } });

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

  fireEvent.click(await screen.findByRole("button", { name: /start fra mal/i }));
  expect((screen.getByLabelText("Søknadsbrev") as HTMLTextAreaElement).value)
    .toContain("Kjære rekrutteringsansvarlig");
  expect(screen.getByRole("button", { name: "Fet" })).toBeInTheDocument();
  expect(screen.getByRole("button", { name: "Lenke" })).toBeInTheDocument();
});

test("AI cover letter suggestions use the linked CV and require explicit apply", async () => {
  routeGet();
  mockedApi.post.mockResolvedValue({
    data: {
      id: 9,
      module: "cover-letter",
      mode: "professional",
      title: "Cover letter · Generate · Professional",
      provider: "local",
      result: { text: "A tailored, truthful suggestion." },
      createdAtUtc: "2026-07-19T11:00:00Z",
    },
  } as any);

  render(<ApplicationCoverLetterSection jobId={7} />);
  expect(await screen.findByText(/application's full job advert and analysis/i)).toBeInTheDocument();
  const assistantSelects = screen.getAllByRole("combobox");
  fireEvent.mouseDown(assistantSelects[assistantSelects.length - 1]);
  fireEvent.click(await screen.findByRole("option", { name: /Norsk bokmål/i }));
  fireEvent.click(screen.getByRole("button", { name: "Generate" }));

  await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith(
    "/jobapplications/7/ai/generate",
    expect.objectContaining({
      module: "cover-letter",
      targetLanguage: "nb-NO",
      currentText: "Dear team",
      action: "generate",
    }),
  ));
  expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team");
  fireEvent.click(await screen.findByRole("button", { name: /Apply to editor/i }));
  expect(screen.getByLabelText("Cover letter")).toHaveValue("A tailored, truthful suggestion.");

  mockedApi.put.mockResolvedValue({ data: { ...coverLetter, text: "A tailored, truthful suggestion." } } as any);
  fireEvent.click(screen.getByRole("button", { name: "Save" }));
  await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith(
    "/jobapplications/7/cover-letter",
    { text: "A tailored, truthful suggestion.", source: "ai", aiAction: "generate" },
  ));
});

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

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

  expect(await screen.findByText(/Could not load this section/i)).toBeInTheDocument();
});

// ---------- Application package drafts ----------

test("application answer and recruiter drafts save from the dedicated workspace", async () => {
  mockedApi.put.mockResolvedValue({ data: undefined } as any);
  const onSaved = jest.fn();

  render(
    <ApplicationPackageDraftsSection
      jobId={7}
      initialApplicationAnswer="Saved answer"
      initialRecruiterMessage="Saved recruiter note"
      onSaved={onSaved}
    />,
  );

  fireEvent.change(screen.getByLabelText("Application answer"), { target: { value: "Edited answer" } });
  fireEvent.change(screen.getByLabelText("Recruiter message"), { target: { value: "Edited recruiter note" } });
  expect(screen.getByText("Unsaved changes")).toBeInTheDocument();
  fireEvent.click(screen.getByRole("button", { name: "Save application drafts" }));

  await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith(
    "/jobapplications/7/application-drafts",
    {
      applicationAnswerDraft: "Edited answer",
      recruiterMessageDraft: "Edited recruiter note",
    },
  ));
  expect(onSaved).toHaveBeenCalled();
});

test("application package drafts can be cleared without deleting ordinary notes", async () => {
  mockedApi.put.mockResolvedValue({ data: undefined } as any);

  render(
    <ApplicationPackageDraftsSection
      jobId={7}
      initialApplicationAnswer="Saved answer"
      initialRecruiterMessage="Saved recruiter note"
    />,
  );

  fireEvent.change(screen.getByLabelText("Application answer"), { target: { value: "" } });
  fireEvent.change(screen.getByLabelText("Recruiter message"), { target: { value: "" } });
  fireEvent.click(screen.getByRole("button", { name: "Save application drafts" }));

  await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith(
    "/jobapplications/7/application-drafts",
    { applicationAnswerDraft: "", recruiterMessageDraft: "" },
  ));
});
