import { createJob } from "@shared/testing/factories";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { scoreJobsStep } from "./score-jobs";

vi.mock("@infra/logger", () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  },
}));

vi.mock("@server/repositories/jobs", () => ({
  getUnscoredDiscoveredJobs: vi.fn(),
  updateJob: vi.fn(),
}));

vi.mock("@server/repositories/settings", () => ({
  getSetting: vi.fn(),
}));

vi.mock("@server/services/scorer", () => ({
  scoreJobSuitability: vi.fn(),
}));

vi.mock("@server/services/visa-sponsors/index", () => ({
  searchSponsors: vi.fn(),
  calculateSponsorMatchSummary: vi.fn(),
}));

vi.mock("../progress", () => ({
  updateProgress: vi.fn(),
  progressHelpers: {
    scoringJob: vi.fn(),
    scoringComplete: vi.fn(),
  },
}));

describe("scoreJobsStep auto-skip behavior", () => {
  beforeEach(async () => {
    vi.clearAllMocks();

    const jobsRepo = await import("@server/repositories/jobs");
    const settingsRepo = await import("@server/repositories/settings");
    const scorer = await import("@server/services/scorer");
    const visaSponsors = await import("@server/services/visa-sponsors/index");

    vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
      createJob({
        title: "Software Engineer",
        employer: "Acme Corp",
        status: "discovered",
        suitabilityScore: null,
        suitabilityReason: null,
      }),
    ]);
    vi.mocked(jobsRepo.updateJob).mockResolvedValue(null);
    vi.mocked(settingsRepo.getSetting).mockResolvedValue(null);
    vi.mocked(scorer.scoreJobSuitability).mockResolvedValue({
      score: 40,
      reason: "Low fit",
      analysis: null,
    });
    vi.mocked(visaSponsors.searchSponsors).mockResolvedValue([]);
    vi.mocked(visaSponsors.calculateSponsorMatchSummary).mockReturnValue({
      sponsorMatchScore: 0,
      sponsorMatchNames: null,
    });
  });

  it("auto-skips jobs when score is below threshold", async () => {
    const settingsRepo = await import("@server/repositories/settings");
    const jobsRepo = await import("@server/repositories/jobs");
    const { logger } = await import("@infra/logger");

    vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");

    await scoreJobsStep({ profile: {}, ownerProfileId: "__default__" });

    expect(jobsRepo.updateJob).toHaveBeenCalledWith(
      "job-1",
      expect.objectContaining({
        suitabilityScore: 40,
        status: "skipped",
      }),
    );
    expect(logger.info).toHaveBeenCalledWith(
      "Auto-skipped job due to low score",
      expect.objectContaining({
        jobId: "job-1",
        score: 40,
        threshold: 50,
      }),
    );
  });

  it("does not auto-skip jobs when score equals threshold", async () => {
    const settingsRepo = await import("@server/repositories/settings");
    const jobsRepo = await import("@server/repositories/jobs");
    const scorer = await import("@server/services/scorer");
    const { logger } = await import("@infra/logger");

    vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");
    vi.mocked(scorer.scoreJobSuitability).mockResolvedValue({
      score: 50,
      reason: "At threshold",
      analysis: null,
    });

    await scoreJobsStep({ profile: {}, ownerProfileId: "__default__" });

    expect(jobsRepo.updateJob).toHaveBeenCalledWith(
      "job-1",
      expect.objectContaining({
        suitabilityScore: 50,
      }),
    );
    const updatePayload = vi.mocked(jobsRepo.updateJob).mock.calls[0][1] as {
      status?: string;
    };
    expect(updatePayload).not.toHaveProperty("status");
    expect(logger.info).not.toHaveBeenCalledWith(
      "Auto-skipped job due to low score",
      expect.anything(),
    );
  });

  it("does not auto-skip when threshold setting is null", async () => {
    const settingsRepo = await import("@server/repositories/settings");
    const jobsRepo = await import("@server/repositories/jobs");

    vi.mocked(settingsRepo.getSetting).mockResolvedValue(null);

    await scoreJobsStep({ profile: {}, ownerProfileId: "__default__" });

    const updatePayload = vi.mocked(jobsRepo.updateJob).mock.calls[0][1] as {
      status?: string;
    };
    expect(updatePayload).not.toHaveProperty("status");
  });

  it("does not auto-skip when threshold setting is NaN", async () => {
    const settingsRepo = await import("@server/repositories/settings");
    const jobsRepo = await import("@server/repositories/jobs");

    vi.mocked(settingsRepo.getSetting).mockResolvedValue("not-a-number");

    await scoreJobsStep({ profile: {}, ownerProfileId: "__default__" });

    const updatePayload = vi.mocked(jobsRepo.updateJob).mock.calls[0][1] as {
      status?: string;
    };
    expect(updatePayload).not.toHaveProperty("status");
  });

  it("never auto-skips applied jobs even when score is below threshold", async () => {
    const settingsRepo = await import("@server/repositories/settings");
    const jobsRepo = await import("@server/repositories/jobs");
    const { logger } = await import("@infra/logger");

    vi.mocked(settingsRepo.getSetting).mockResolvedValue("50");
    vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
      createJob({
        id: "job-applied",
        status: "applied",
        title: "Software Engineer",
        employer: "Acme Corp",
        suitabilityScore: null,
        suitabilityReason: null,
      }),
    ]);

    await scoreJobsStep({ profile: {}, ownerProfileId: "__default__" });

    expect(jobsRepo.updateJob).toHaveBeenCalledWith(
      "job-applied",
      expect.any(Object),
    );
    const updatePayload = vi.mocked(jobsRepo.updateJob).mock.calls[0][1] as {
      status?: string;
    };
    expect(updatePayload).not.toHaveProperty("status");
    expect(logger.info).not.toHaveBeenCalledWith(
      "Auto-skipped job due to low score",
      expect.objectContaining({ jobId: "job-applied" }),
    );
  });

  it("scores multiple jobs and reports completion progress", async () => {
    const jobsRepo = await import("@server/repositories/jobs");
    const scorer = await import("@server/services/scorer");
    const { progressHelpers } = await import("../progress");

    vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
      createJob({
        id: "job-1",
        title: "First Role",
        employer: "Acme",
        suitabilityScore: null,
      }),
      createJob({
        id: "job-2",
        title: "Second Role",
        employer: "Beta",
        suitabilityScore: null,
      }),
    ]);

    vi.mocked(scorer.scoreJobSuitability)
      .mockResolvedValueOnce({
        score: 61,
        reason: "First score",
        analysis: null,
      })
      .mockResolvedValueOnce({
        score: 72,
        reason: "Second score",
        analysis: null,
      });

    const result = await scoreJobsStep({
      profile: {},
      ownerProfileId: "__default__",
    });

    expect(result.scoredJobs).toHaveLength(2);
    expect(vi.mocked(jobsRepo.updateJob)).toHaveBeenCalledTimes(2);
    expect(vi.mocked(progressHelpers.scoringJob)).toHaveBeenCalledTimes(2);
    expect(vi.mocked(progressHelpers.scoringComplete)).toHaveBeenCalledWith(2);
  });

  it("stops before processing when cancellation is requested", async () => {
    const jobsRepo = await import("@server/repositories/jobs");
    const scorer = await import("@server/services/scorer");

    vi.mocked(jobsRepo.getUnscoredDiscoveredJobs).mockResolvedValue([
      createJob({
        id: "job-1",
        title: "Cancelled Role",
        employer: "Acme",
        suitabilityScore: null,
      }),
    ]);

    const result = await scoreJobsStep({
      profile: {},
      ownerProfileId: "__default__",
      shouldCancel: () => true,
    });

    expect(result.scoredJobs).toHaveLength(0);
    expect(vi.mocked(scorer.scoreJobSuitability)).not.toHaveBeenCalled();
    expect(vi.mocked(jobsRepo.updateJob)).not.toHaveBeenCalled();
  });
});
