import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const llmMocks = vi.hoisted(() => ({
  generateValidatedJson: vi.fn(),
}));

vi.mock("../../../lib/llm/client", async () => {
  const actual = await vi.importActual<typeof import("../../../lib/llm/client")>(
    "../../../lib/llm/client",
  );
  return {
    ...actual,
    generateValidatedJson: llmMocks.generateValidatedJson,
  };
});

import { POST as confirmFactCard } from "../jobs/[jobId]/confirm-fact-card/route";
import { GET as downloadExport } from "../jobs/[jobId]/exports/[fileName]/route";
import { POST as optimizeJob } from "../jobs/[jobId]/optimize/route";
import { GET as getJobProgress } from "../jobs/[jobId]/progress/route";
import { POST as createJob } from "../jobs/route";

const validFactCard = {
  company_full_name: "Example Technology Co., Ltd.",
  company_short_names: ["Example Tech"],
  brand_names: ["Example"],
  product_names: ["Example GEO"],
  target_industry: "GEO optimization",
  target_audience: "Marketing teams",
  experience_years: 8,
  core_claims: ["Eight years of GEO optimization experience"],
  forbidden_claims: [],
  image_topics: ["Product dashboard"],
  uncertain_items: [],
  is_ready_for_optimization: true,
  confirmed_by_user: true,
};

const validCandidateFactCard = {
  company_full_name: validFactCard.company_full_name,
  company_short_names: validFactCard.company_short_names,
  brand_names: validFactCard.brand_names,
  product_names: validFactCard.product_names,
  target_industry: validFactCard.target_industry,
  target_audience: validFactCard.target_audience,
  experience_years: validFactCard.experience_years,
  core_claims: validFactCard.core_claims,
  forbidden_claims: validFactCard.forbidden_claims,
  image_topics: validFactCard.image_topics,
  uncertain_items: validFactCard.uncertain_items,
  is_ready_for_optimization: true,
};

interface TimingStepResponse {
  label: string;
  duration_ms: number;
}

interface TimingResponse {
  total_ms: number;
  steps: TimingStepResponse[];
}

interface CreateJobResponse {
  job: { id: string };
  candidateFactCard: { company_full_name: string };
  timing: TimingResponse;
}

interface OptimizeJobResponse {
  optimizedArticle: { title: string };
  qaReport: { checks: unknown[]; overall_status?: string };
  exportPaths?: Record<string, string>;
  timing: TimingResponse;
}

describe("job API routes", () => {
  let tempDir: string;
  const originalDataDir = process.env.APP_DATA_DIR;
  const originalApiKey = process.env.API_ACCESS_KEY;
  const originalAuthDisabled = process.env.API_AUTH_DISABLED;

  beforeEach(() => {
    tempDir = mkdtempSync(join(tmpdir(), "geo-agent-api-"));
    process.env.APP_DATA_DIR = tempDir;
    process.env.API_ACCESS_KEY = "test-key";
    process.env.API_AUTH_DISABLED = "false";
  });

  afterEach(() => {
    process.env.APP_DATA_DIR = originalDataDir;
    process.env.API_ACCESS_KEY = originalApiKey;
    process.env.API_AUTH_DISABLED = originalAuthDisabled;
    llmMocks.generateValidatedJson.mockReset();
    rmSync(tempDir, { recursive: true, force: true });
  });

  it("rejects API requests without the access key", async () => {
    const response = await createJob(
      request(
        {
          title: "Example Technology Co., Ltd. GEO guide",
          body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
          platform: "official_site",
        },
        { apiKey: null },
      ),
    );

    expect(response.status).toBe(401);
  });

  it("validates input, creates a job, and returns a candidate fact card", async () => {
    llmMocks.generateValidatedJson.mockResolvedValueOnce(validCandidateFactCard);

    const response = await createJob(
      request({
        title: "Example Technology Co., Ltd. GEO guide",
        body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
        image_lines: "Product dashboard",
        platform: "official_site",
        user_instructions: "Keep factual",
      }),
    );
    const body = (await response.json()) as CreateJobResponse;

    expect(response.status).toBe(201);
    expect(body.job.id).toMatch(/^job_/);
    expect(body.candidateFactCard.company_full_name).toBe(
      "Example Technology Co., Ltd.",
    );
    expect(body.timing.total_ms).toBeGreaterThanOrEqual(0);
    expect(body.timing.steps).toEqual([
      expect.objectContaining({
        label: "事实卡提取",
        duration_ms: expect.any(Number),
      }),
    ]);
  });

  it("returns a clear error when LLM fact extraction fails", async () => {
    llmMocks.generateValidatedJson.mockRejectedValueOnce(
      new Error("LLM response failed schema validation: target_audience"),
    );

    const response = await createJob(
      request({
        title: "Example Technology Co., Ltd. GEO guide",
        body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
        platform: "official_site",
      }),
    );
    const body = (await response.json()) as { error: string; timing: TimingResponse };

    expect(response.status).toBe(502);
    expect(body.error).toBe(
      "LLM response failed schema validation: target_audience",
    );
    expect(body.timing.total_ms).toBeGreaterThanOrEqual(0);
    expect(body.timing.steps).toEqual([
      expect.objectContaining({
        label: "事实卡提取",
        duration_ms: expect.any(Number),
      }),
    ]);
  });

  it("still returns 400 for invalid article input", async () => {
    const response = await createJob(
      request({
        title: "",
        body: "",
        platform: "official_site",
      }),
    );

    expect(response.status).toBe(400);
  });

  it("rejects unresolved uncertain items when confirming a fact card", async () => {
    const { job } = await createJobFixture();
    const response = await confirmFactCard(
      request({
        ...validFactCard,
        uncertain_items: ["Need company confirmation"],
        is_ready_for_optimization: false,
      }),
      params<{ jobId: string }>({ jobId: job.id }),
    );

    expect(response.status).toBe(400);
  });

  it("rejects optimize requests for jobs without confirmed fact cards", async () => {
    const { job } = await createJobFixture();
    const response = await optimizeJob(
      request({}),
      params<{ jobId: string }>({ jobId: job.id }),
    );

    expect(response.status).toBe(409);
  });

  it("returns optimized article and QA report for successful optimization", async () => {
    const { job } = await createJobFixture();
    await confirmFactCard(
      request(validFactCard),
      params<{ jobId: string }>({ jobId: job.id }),
    );

    llmMocks.generateValidatedJson
      .mockResolvedValueOnce({
        title: "API LLM Optimized GEO Article",
        summary:
          "A official site article for Marketing teams about GEO optimization.",
        body_markdown:
          "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
        image_suggestions: [],
        changed_sections: ["title", "body"],
        requires_user_confirmation: [],
      })
      .mockResolvedValueOnce({ checks: [] });

    const response = await optimizeJob(
      request({}),
      params<{ jobId: string }>({ jobId: job.id }),
    );
    const body = (await response.json()) as OptimizeJobResponse;

    expect(response.status).toBe(200);
    expect(body.optimizedArticle.title).toBe("API LLM Optimized GEO Article");
    expect(body.qaReport.checks).toHaveLength(10);
    expect(body.timing.total_ms).toBeGreaterThanOrEqual(0);
    expect(body.timing.steps.map((step) => step.label)).toEqual([
      "生成优化稿",
      "质量检查",
      "整理结果",
    ]);
    expect(body.timing.steps.every((step) => step.duration_ms >= 0)).toBe(true);
  });

  it("returns backend-visible optimization progress after a job runs", async () => {
    const { job } = await createJobFixture();
    await confirmFactCard(
      request(validFactCard),
      params<{ jobId: string }>({ jobId: job.id }),
    );

    llmMocks.generateValidatedJson
      .mockResolvedValueOnce({
        title: "API LLM Optimized GEO Article",
        summary:
          "A official site article for Marketing teams about GEO optimization.",
        body_markdown:
          "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
        image_suggestions: [],
        changed_sections: ["title", "body"],
        requires_user_confirmation: [],
      })
      .mockResolvedValueOnce({ checks: [] });

    await optimizeJob(request({}), params<{ jobId: string }>({ jobId: job.id }));

    const response = await getJobProgress(
      request({}),
      params<{ jobId: string }>({ jobId: job.id }),
    );
    const body = (await response.json()) as {
      progress: { status: string; steps: Array<{ label: string; status: string }> };
    };

    expect(response.status).toBe(200);
    expect(body.progress.status).toBe("completed");
    expect(body.progress.steps.map((step) => step.label)).toEqual([
      "生成优化稿",
      "质量检查",
      "整理结果",
    ]);
  });

  it("uses mocked LLM article output during optimize route", async () => {
    const { job } = await createJobFixture();
    await confirmFactCard(
      request(validFactCard),
      params<{ jobId: string }>({ jobId: job.id }),
    );

    llmMocks.generateValidatedJson
      .mockResolvedValueOnce({
        title: "API LLM Optimized GEO Article",
        summary:
          "A official site article for Marketing teams about GEO optimization.",
        body_markdown:
          "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
        image_suggestions: [
          { source: "image_1", suggestion: "Use Product dashboard." },
        ],
        changed_sections: ["title", "body"],
        requires_user_confirmation: [],
      })
      .mockResolvedValue({ checks: [] });

    const response = await optimizeJob(
      request({}),
      params<{ jobId: string }>({ jobId: job.id }),
    );
    const body = (await response.json()) as OptimizeJobResponse;

    expect(response.status).toBe(200);
    expect(body.optimizedArticle.title).toBe("API LLM Optimized GEO Article");
    expect(llmMocks.generateValidatedJson).toHaveBeenCalled();
  });

  it("still writes export files when the QA report fails", async () => {
    const { job } = await createJobFixture();
    await confirmFactCard(
      request(validFactCard),
      params<{ jobId: string }>({ jobId: job.id }),
    );

    llmMocks.generateValidatedJson
      .mockResolvedValueOnce({
        title: "API LLM Optimized GEO Article!!!",
        summary:
          "A official site article for Marketing teams about GEO optimization.",
        body_markdown:
          "Example has 12 years of GEO optimization experience and 99 patents.",
        image_suggestions: [],
        changed_sections: ["title", "body"],
        requires_user_confirmation: [],
      })
      .mockResolvedValueOnce({ checks: [] })
      .mockResolvedValueOnce({
        title: "API LLM Optimized GEO Article",
        summary:
          "A official site article for Marketing teams about GEO optimization.",
        body_markdown:
          "Example has 12 years of GEO optimization experience and 99 patents.",
        image_suggestions: [],
        changed_sections: ["body"],
        requires_user_confirmation: [],
      })
      .mockResolvedValueOnce({ checks: [] })
      .mockResolvedValueOnce({
        title: "API LLM Optimized GEO Article",
        summary:
          "A official site article for Marketing teams about GEO optimization.",
        body_markdown:
          "Example has 12 years of GEO optimization experience and 99 patents.",
        image_suggestions: [],
        changed_sections: ["body"],
        requires_user_confirmation: [],
      })
      .mockResolvedValueOnce({ checks: [] });

    const response = await optimizeJob(
      request({}),
      params<{ jobId: string }>({ jobId: job.id }),
    );
    const body = (await response.json()) as OptimizeJobResponse;

    expect(response.status).toBe(200);
    expect(body.qaReport.overall_status).toBe("fail");
    expect(body.exportPaths).toEqual(
      expect.objectContaining({
        markdown: expect.stringContaining("optimized.md"),
        docx: expect.stringContaining("optimized.docx"),
        qaJson: expect.stringContaining("qa_report.json"),
      }),
    );
  });

  it("returns a clear error when LLM optimization fails", async () => {
    const { job } = await createJobFixture();
    await confirmFactCard(
      request(validFactCard),
      params<{ jobId: string }>({ jobId: job.id }),
    );

    llmMocks.generateValidatedJson.mockRejectedValueOnce(
      new Error("LLM response failed schema validation: body_markdown"),
    );

    const response = await optimizeJob(
      request({}),
      params<{ jobId: string }>({ jobId: job.id }),
    );
    const body = (await response.json()) as { error: string; timing: TimingResponse };

    expect(response.status).toBe(502);
    expect(body.error).toBe("LLM response failed schema validation: body_markdown");
    expect(body.timing.total_ms).toBeGreaterThanOrEqual(0);
    expect(body.timing.steps).toEqual([]);
  });

  it("rejects unknown export filenames", async () => {
    const { job } = await createJobFixture();
    const exportDir = join(tempDir, "exports", job.id);
    mkdirSync(exportDir, { recursive: true });
    writeFileSync(join(exportDir, "optimized.md"), "# Optimized");

    const response = await downloadExport(
      request({}),
      params<{ jobId: string; fileName: string }>({
        jobId: job.id,
        fileName: "unknown.txt",
      }),
    );

    expect(response.status).toBe(404);
  });
});

async function createJobFixture() {
  llmMocks.generateValidatedJson.mockResolvedValueOnce(validCandidateFactCard);

  const response = await createJob(
    request({
      title: "Example Technology Co., Ltd. GEO guide",
      body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
      image_lines: "Product dashboard",
      platform: "official_site",
      user_instructions: "",
    }),
  );
  return response.json() as Promise<{ job: { id: string } }>;
}

function request(body: unknown, options: { apiKey?: string | null } = {}) {
  const headers: Record<string, string> = { "content-type": "application/json" };
  const apiKey = options.apiKey === undefined ? "test-key" : options.apiKey;
  if (apiKey) {
    headers["x-api-key"] = apiKey;
  }

  return new Request("http://localhost/api/jobs", {
    method: "POST",
    body: JSON.stringify(body),
    headers,
  });
}

function params<T extends Record<string, string>>(values: T) {
  return { params: Promise.resolve(values) };
}
