import { beforeEach, describe, expect, it, vi } from "vitest";
import { generatePdf } from "./pdf";
import { resolveLocalResumeFilePath } from "./profile";
import * as projectSelection from "./projectSelection";
import { getResume } from "./rxresume";

// Define mock data in hoisted block
const { mocks, mockProfile, mockRxResume } = vi.hoisted(() => {
  const profile = {
    sections: {
      summary: { content: "Original Summary" },
      skills: { items: ["Original Skill"] },
      projects: {
        items: [
          // Start with visible=true to test if they get hidden
          { id: "p1", name: "Project 1", visible: true },
          { id: "p2", name: "Project 2", visible: true },
        ],
      },
    },
    basics: { headline: "Original Headline" },
  };

  // Capture what's passed to create()
  let lastCreateData: any = null;

  const mockRxResumeApi = {
    importResume: vi.fn().mockImplementation((payload: any) => {
      const data = payload?.data;
      lastCreateData = JSON.parse(JSON.stringify(data)); // Deep clone
      return Promise.resolve("mock-resume-id");
    }),
    exportResumePdf: vi
      .fn()
      .mockResolvedValue("https://example.com/pdf/mock.pdf"),
    deleteResume: vi.fn().mockResolvedValue(undefined),
    getLastCreateData: () => lastCreateData,
    clearLastCreateData: () => {
      lastCreateData = null;
    },
  };

  return {
    mockProfile: profile,
    mocks: {
      readFile: vi.fn(),
      writeFile: vi.fn(),
      mkdir: vi.fn().mockResolvedValue(undefined),
      access: vi.fn().mockResolvedValue(undefined),
      unlink: vi.fn().mockResolvedValue(undefined),
    },
    mockRxResume: mockRxResumeApi,
  };
});

// Configure base mock implementations
mocks.readFile.mockResolvedValue(JSON.stringify(mockProfile));
mocks.writeFile.mockResolvedValue(undefined);

vi.mock("fs/promises", async () => {
  return {
    default: mocks,
    ...mocks,
  };
});

vi.mock("node:fs/promises", async () => {
  return {
    default: mocks,
    ...mocks,
  };
});

vi.mock("fs", () => ({
  existsSync: vi.fn().mockReturnValue(true),
  createWriteStream: vi.fn().mockReturnValue({
    on: vi.fn(),
    write: vi.fn(),
    end: vi.fn(),
  }),
  default: {
    existsSync: vi.fn().mockReturnValue(true),
    createWriteStream: vi.fn().mockReturnValue({
      on: vi.fn(),
      write: vi.fn(),
      end: vi.fn(),
    }),
  },
}));

vi.mock("node:fs", () => ({
  existsSync: vi.fn().mockReturnValue(true),
  createWriteStream: vi.fn().mockReturnValue({
    on: vi.fn(),
    write: vi.fn(),
    end: vi.fn(),
  }),
  default: {
    existsSync: vi.fn().mockReturnValue(true),
    createWriteStream: vi.fn().mockReturnValue({
      on: vi.fn(),
      write: vi.fn(),
      end: vi.fn(),
    }),
  },
}));

vi.mock("../repositories/settings", () => ({
  getSetting: vi.fn().mockImplementation((key: string) => {
    if (key === "rxresumeEmail") return Promise.resolve("test@example.com");
    if (key === "rxresumePassword") return Promise.resolve("testpassword");
    return Promise.resolve(null);
  }),
  getAllSettings: vi.fn().mockResolvedValue({}),
}));

// Mock the profile service - getProfile now fetches from v4 API
vi.mock("./profile", () => ({
  getProfile: vi.fn().mockResolvedValue(mockProfile),
  resolveLocalResumeFilePath: vi.fn().mockResolvedValue(null),
}));

vi.mock("./projectSelection", () => ({
  pickProjectIdsForJob: vi.fn().mockResolvedValue([]),
}));

vi.mock("./resumeProjects", () => ({
  extractProjectsFromProfile: vi.fn().mockReturnValue({
    catalog: [],
    selectionItems: [
      { id: "p1", name: "Project 1" },
      { id: "p2", name: "Project 2" },
    ],
  }),
  resolveResumeProjectsSettings: vi.fn().mockReturnValue({
    resumeProjects: {
      lockedProjectIds: [],
      aiSelectableProjectIds: ["p1", "p2"],
      maxProjects: 3,
    },
  }),
}));

const mockTracerLinks = vi.hoisted(() => ({
  resolveTracerPublicBaseUrl: vi.fn().mockReturnValue("https://jobops.example"),
  rewriteResumeLinksWithTracer: vi
    .fn()
    .mockResolvedValue({ rewrittenLinks: 2 }),
}));

vi.mock("./tracer-links", () => ({
  resolveTracerPublicBaseUrl: mockTracerLinks.resolveTracerPublicBaseUrl,
  rewriteResumeLinksWithTracer: mockTracerLinks.rewriteResumeLinksWithTracer,
}));

vi.mock("./rxresume/baseResumeId", () => ({
  getConfiguredRxResumeBaseResumeId: vi.fn().mockResolvedValue({
    mode: "v4",
    resumeId: "base-resume-id",
  }),
}));

vi.mock("./rxresume", async () => {
  const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value));
  const projectSelectionModule = await import("./projectSelection");
  return {
    getResume: vi.fn().mockResolvedValue({
      id: "base-resume-id",
      name: "Base Resume",
      mode: "v4",
      data: mockProfile,
    }),
    prepareTailoredResumeForPdf: vi
      .fn()
      .mockImplementation(async (args: any) => {
        const data = clone(args.resumeData);
        if (args.tailedContent?.summary || args.tailoredContent?.summary) {
          const summary = args.tailoredContent?.summary;
          if (data.sections?.summary) data.sections.summary.content = summary;
        }
        if (args.tailoredContent?.headline && data.basics) {
          data.basics.headline = args.tailoredContent.headline;
        }

        let selected = (args.selectedProjectIds as string | null | undefined)
          ?.split(",")
          .map((s) => s.trim())
          .filter(Boolean);
        if (!selected) {
          selected = await projectSelectionModule.pickProjectIdsForJob({
            jobDescription: args.jobDescription,
            eligibleProjects: [
              { id: "p1", name: "Project 1" },
              { id: "p2", name: "Project 2" },
            ],
            desiredCount: 3,
          } as any);
        }
        const selectedSet = new Set(selected);
        for (const item of data.sections?.projects?.items ?? []) {
          item.visible = selectedSet.has(item.id);
        }
        if (data.sections?.projects) data.sections.projects.visible = true;

        if (args.tracerLinks?.enabled) {
          mockTracerLinks.resolveTracerPublicBaseUrl({
            requestOrigin: args.tracerLinks.requestOrigin,
          });
          await mockTracerLinks.rewriteResumeLinksWithTracer({
            jobId: args.jobId,
            resumeData: data,
            publicBaseUrl: "https://jobops.example",
            companyName: args.tracerLinks.companyName ?? null,
          });
        }

        return {
          mode: "v4",
          data,
          projectCatalog: [],
          selectedProjectIds: [...selectedSet],
        };
      }),
    importResume: mockRxResume.importResume,
    exportResumePdf: mockRxResume.exportResumePdf,
    deleteResume: mockRxResume.deleteResume,
  };
});

// Mock stream pipeline for downloading PDF
vi.mock("stream/promises", () => ({
  pipeline: vi.fn().mockResolvedValue(undefined),
  default: {
    pipeline: vi.fn().mockResolvedValue(undefined),
  },
}));

vi.mock("node:stream/promises", () => ({
  pipeline: vi.fn().mockResolvedValue(undefined),
  default: {
    pipeline: vi.fn().mockResolvedValue(undefined),
  },
}));

// Mock stream Readable
vi.mock("stream", () => ({
  Readable: {
    fromWeb: vi.fn().mockReturnValue({
      pipe: vi.fn(),
    }),
  },
  default: {
    Readable: {
      fromWeb: vi.fn().mockReturnValue({
        pipe: vi.fn(),
      }),
    },
  },
}));

vi.mock("node:stream", () => ({
  Readable: {
    fromWeb: vi.fn().mockReturnValue({
      pipe: vi.fn(),
    }),
  },
  default: {
    Readable: {
      fromWeb: vi.fn().mockReturnValue({
        pipe: vi.fn(),
      }),
    },
  },
}));

// Mock global fetch
vi.stubGlobal(
  "fetch",
  vi.fn().mockResolvedValue({
    ok: true,
    body: {},
  }),
);

describe("PDF Service Tailoring Logic", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(resolveLocalResumeFilePath).mockResolvedValue(null);
    mocks.readFile.mockResolvedValue(JSON.stringify(mockProfile));
    mockRxResume.clearLastCreateData();
    mockTracerLinks.resolveTracerPublicBaseUrl.mockReturnValue(
      "https://jobops.example",
    );
    mockTracerLinks.rewriteResumeLinksWithTracer.mockResolvedValue({
      rewrittenLinks: 2,
    });
  });

  it("loads base resume from local file when a local path is configured", async () => {
    vi.mocked(resolveLocalResumeFilePath).mockResolvedValue(
      "/tmp/jobops-base.json",
    );
    mocks.readFile.mockResolvedValue(JSON.stringify(mockProfile));

    await generatePdf("job-local-base", {}, "desc", undefined, "p1");

    expect(getResume).not.toHaveBeenCalled();
    expect(mockRxResume.importResume).toHaveBeenCalled();
  });

  it("should use provided selectedProjectIds and BYPASS AI selection", async () => {
    const tailoredContent = {
      summary: "New Sum",
      headline: "New Head",
      skills: [],
    };

    await generatePdf("job-1", tailoredContent, "Job Desc", "base.json", "p2");

    // 1. pickProjectIdsForJob should NOT be called
    expect(projectSelection.pickProjectIdsForJob).not.toHaveBeenCalled();

    // 2. Verify create data content
    expect(mockRxResume.importResume).toHaveBeenCalled();
    const savedResumeJson = mockRxResume.getLastCreateData();

    const projects = savedResumeJson.sections.projects.items;
    const p1 = projects.find((p: any) => p.id === "p1");
    const p2 = projects.find((p: any) => p.id === "p2");

    expect(p2.visible).toBe(true);
    expect(p1.visible).toBe(false);

    // 3. Verify Summary Update
    const summary = savedResumeJson.sections.summary.content;
    expect(summary).toBe("New Sum");
  });

  it("should handle comma-separated project IDs correctly", async () => {
    await generatePdf("job-2", {}, "desc", "base.json", "p1, p2 ");

    expect(mockRxResume.importResume).toHaveBeenCalled();
    const savedResumeJson = mockRxResume.getLastCreateData();
    const projects = savedResumeJson.sections.projects.items;

    expect(projects.find((p: any) => p.id === "p1").visible).toBe(true);
    expect(projects.find((p: any) => p.id === "p2").visible).toBe(true);
  });

  it("keeps projects section visible when selected project list is explicitly empty", async () => {
    await generatePdf("job-empty-projects", {}, "desc", "base.json", "");

    expect(mockRxResume.importResume).toHaveBeenCalled();
    const savedResumeJson = mockRxResume.getLastCreateData();
    const projects = savedResumeJson.sections.projects.items;

    expect(projects.find((p: any) => p.id === "p1").visible).toBe(false);
    expect(projects.find((p: any) => p.id === "p2").visible).toBe(false);
    expect(savedResumeJson.sections.projects.visible).toBe(true);
  });

  it("should fall back to AI selection if selectedProjectIds is null/undefined", async () => {
    // Setup AI selection mock for this test
    vi.mocked(projectSelection.pickProjectIdsForJob).mockResolvedValue(["p1"]);

    await generatePdf("job-3", {}, "desc", "base.json", undefined);

    expect(projectSelection.pickProjectIdsForJob).toHaveBeenCalled();

    expect(mockRxResume.importResume).toHaveBeenCalled();
    const savedResumeJson = mockRxResume.getLastCreateData();

    const p1 = savedResumeJson.sections.projects.items.find(
      (p: any) => p.id === "p1",
    );
    const p2 = savedResumeJson.sections.projects.items.find(
      (p: any) => p.id === "p2",
    );

    expect(p1.visible).toBe(true);
    expect(p2.visible).toBe(false);

    const visibleCount = savedResumeJson.sections.projects.items.filter(
      (p: any) => p.visible,
    ).length;
    expect(visibleCount).toBe(1);
  });

  it("does not rewrite links when tracer links are disabled", async () => {
    await generatePdf("job-no-tracer", {}, "desc", undefined, undefined, {
      tracerLinksEnabled: false,
    });

    expect(mockTracerLinks.resolveTracerPublicBaseUrl).not.toHaveBeenCalled();
    expect(mockTracerLinks.rewriteResumeLinksWithTracer).not.toHaveBeenCalled();
  });

  it("rewrites links when tracer links are enabled", async () => {
    await generatePdf("job-with-tracer", {}, "desc", undefined, undefined, {
      tracerLinksEnabled: true,
      requestOrigin: "https://jobops.example",
    });

    expect(mockTracerLinks.resolveTracerPublicBaseUrl).toHaveBeenCalledWith({
      requestOrigin: "https://jobops.example",
    });
    expect(mockTracerLinks.rewriteResumeLinksWithTracer).toHaveBeenCalledTimes(
      1,
    );
  });
});
