import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createNativeScannerProcessingService } from "../../electron/services/nativeScannerProcessingService";
import type { NativeCaptureJobPayload } from "../../electron/services/nativeScannerProcessingService";
import type { IkArtifactCatalog } from "../lib/ikArtifactMatcher";
import type { StoredArtifactRecord, StoredScanResultEntry } from "../types/storage";
import { captureFromOcr } from "./ocrEvalHarness";

const tempDirs: string[] = [];

async function makeRunDir() {
  const runDir = await fs.mkdtemp(path.join(os.tmpdir(), "gaa-native-process-"));
  tempDirs.push(runDir);
  return runDir;
}

async function writeJobs(runDir: string, jobs: NativeCaptureJobPayload[]) {
  const lines = jobs.map((job) => JSON.stringify(job)).join("\n");
  await fs.writeFile(path.join(runDir, "capture-jobs.jsonl"), `${lines}\n`, "utf8");
}

async function writeCrop(runDir: string, relativePath = "cards/artifact-0001.png") {
  const absolutePath = path.join(runDir, relativePath);
  await fs.mkdir(path.dirname(absolutePath), { recursive: true });
  await fs.writeFile(absolutePath, "test image placeholder", "utf8");
  return { relativePath, absolutePath };
}

async function writePreviewPng(runDir: string, relativePath = "cards/preview.png") {
  const absolutePath = path.join(runDir, relativePath);
  await fs.mkdir(path.dirname(absolutePath), { recursive: true });
  await fs.writeFile(
    absolutePath,
    Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==", "base64"),
  );
  return { relativePath, absolutePath };
}

function safePlumeCapture() {
  return captureFromOcr({
    "artifact-name": "Pristine Plume of the Blessed",
    "artifact-slot": "Plume of Death",
    "artifact-main-stat-label": "ATK",
    "artifact-level": "+20",
    "artifact-substats": "+ CRIT DMG+7.0%\n+ DEF+30.6%\n+ Elemental Mastery+40\n+ ATK+5.8%",
    "artifact-set-effects": "Silken Moon's Serenade:\n2-Piece Set: Energy Recharge +20%.",
    "artifact-footer": "Equipped: Aino",
  });
}

function safePlumeIkCatalog(overrides: Partial<IkArtifactCatalog["artifacts"][number]["pieces"][number]> = {}): IkArtifactCatalog {
  return {
    artifacts: [
      {
        normalizedName: "silkenmoonsserenade",
        setName: "Silken Moon's Serenade",
        good: "SilkenMoonsSerenade",
        pieces: [
          {
            slot: "plume",
            artifactName: "Pristine Plume of the Blessed",
            good: "PristinePlumeOfTheBlessed",
            normalizedName: "pristineplumeoftheblessed",
            ...overrides,
          },
        ],
      },
    ],
  };
}

function reviewOnlyCapture() {
  return captureFromOcr({
    "artifact-title": "Moonlit Offering's Final\nSands of Eon",
    "artifact-main-stat": "46.6%\n1S 2.5.8 J\nSe",
    "artifact-substats": "+ ATK+19\n+ Energy Recharge+6.5%\n+ CRIT DMG+18.7%",
    "artifact-set-effects": "Aubade of Morningstar and Moon\n2-Piece Set: Increases Elemental Mastery by 80.",
    "artifact-footer": "",
  });
}

afterEach(async () => {
  await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
});

describe("native scanner processing service", () => {
  it("reads native capture jobs and writes a processing report without persisting by default", async () => {
    const runDir = await makeRunDir();
    const { relativePath, absolutePath } = await writeCrop(runDir);
    await writeJobs(runDir, [{ sequence: 7, page: 2, row: 1, col: 3, relativePath }]);

    const savedRecords: StoredArtifactRecord[] = [];
    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async (imagePath, job) => {
        expect(imagePath).toBe(absolutePath);
        expect(job.sequence).toBe(7);
        return safePlumeCapture();
      },
      saveArtifacts: async (records) => {
        savedRecords.push(...records);
        return { added: records.length, updated: 0 };
      },
    });

    const status = await service.processRun();
    const loadedResults = await service.loadResults();
    const report = JSON.parse(await fs.readFile(path.join(runDir, "processing-report.json"), "utf8"));
    const scanResults = JSON.parse(await fs.readFile(path.join(runDir, "scan-results.json"), "utf8"));

    expect(status.ok).toBe(true);
    expect(status.scanResultsPath).toBe(path.join(runDir, "scan-results.json"));
    expect(status.processed).toBe(1);
    expect(status.parsed).toBe(1);
    expect(status.stored).toBe(0);
    expect(status.persisted).toBe(false);
    expect(status.results[0]).toMatchObject({
      sequence: 7,
      page: 2,
      row: 1,
      col: 3,
      imagePath: absolutePath,
      parsed: true,
      artifactName: "Pristine Plume of the Blessed",
      setName: "Silken Moon's Serenade",
      slot: "Plume of Death",
      persisted: false,
    });
    expect(report.processed).toBe(1);
    expect(loadedResults).toMatchObject({
      ok: true,
      runDir,
      path: path.join(runDir, "scan-results.json"),
      total: 1,
    });
    expect(loadedResults.results[0]).toMatchObject({
      sequence: 7,
      extractionStatus: "parsed",
      valueStatus: "deferred",
    });
    expect(scanResults).toHaveLength(1);
    expect(scanResults[0]).toMatchObject({
      runId: path.basename(runDir),
      sequence: 7,
      category: "artifact",
      source: "native-ik-scan",
      extractionStatus: "parsed",
      valueStatus: "deferred",
      valueScore: null,
      persistedArtifact: false,
      artifact: {
        name: "Pristine Plume of the Blessed",
        slot: "Plume of Death",
        setName: "Silken Moon's Serenade",
      },
    });
    expect(savedRecords).toHaveLength(0);
  });

  it("returns a safe empty status when native scan results are not available", async () => {
    const runDir = await makeRunDir();
    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => safePlumeCapture(),
      saveArtifacts: async () => ({ added: 0, updated: 0 }),
    });

    const loadedResults = await service.loadResults();

    expect(loadedResults.ok).toBe(false);
    expect(loadedResults.runDir).toBe(runDir);
    expect(loadedResults.path).toBe(path.join(runDir, "scan-results.json"));
    expect(loadedResults.results).toEqual([]);
  });

  it("loads native crop previews only from inside the run directory", async () => {
    const runDir = await makeRunDir();
    const { absolutePath } = await writePreviewPng(runDir);
    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => safePlumeCapture(),
      saveArtifacts: async () => ({ added: 0, updated: 0 }),
    });

    const preview = await service.loadImage({ imagePath: absolutePath });
    const blocked = await service.loadImage({ imagePath: path.join(os.tmpdir(), "outside.png") });

    expect(preview).toMatchObject({
      ok: true,
      runDir,
      path: absolutePath,
      width: 1,
      height: 1,
    });
    expect(preview.dataUrl).toMatch(/^data:image\/png;base64,/);
    expect(blocked.ok).toBe(false);
    expect(blocked.error).toMatch(/outside/);
  });

  it("marks missing crop images as review errors and skips OCR processing", async () => {
    const runDir = await makeRunDir();
    await writeJobs(runDir, [{ sequence: 1, relativePath: "cards/missing.png" }]);
    let buildCalls = 0;

    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => {
        buildCalls += 1;
        return safePlumeCapture();
      },
      saveArtifacts: async () => ({ added: 0, updated: 0 }),
    });

    const status = await service.processRun({ persist: true });
    const scanResults = JSON.parse(await fs.readFile(path.join(runDir, "scan-results.json"), "utf8"));

    expect(status.ok).toBe(true);
    expect(status.processed).toBe(1);
    expect(status.parsed).toBe(0);
    expect(status.review).toBe(1);
    expect(status.errors).toBe(1);
    expect(status.stored).toBe(0);
    expect(status.results[0]).toMatchObject({
      sequence: 1,
      parsed: false,
      needsReview: true,
      error: "Card crop image missing.",
    });
    expect(scanResults[0]).toMatchObject({
      extractionStatus: "missing_crop",
      valueStatus: "review",
      needsReview: true,
      error: "Card crop image missing.",
    });
    expect(buildCalls).toBe(0);
  });

  it("blocks non-artifact native jobs before OCR processing", async () => {
    const runDir = await makeRunDir();
    const { relativePath, absolutePath } = await writeCrop(runDir, "cards/weapon-0001.png");
    await writeJobs(runDir, [{ sequence: 2, category: "weapons", page: 1, row: 0, col: 1, relativePath }]);
    let buildCalls = 0;

    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => {
        buildCalls += 1;
        return safePlumeCapture();
      },
      saveArtifacts: async () => ({ added: 0, updated: 0 }),
    });

    const status = await service.processRun({ persist: true });
    const scanResults = JSON.parse(await fs.readFile(path.join(runDir, "scan-results.json"), "utf8"));

    expect(status.ok).toBe(true);
    expect(status.processed).toBe(1);
    expect(status.parsed).toBe(0);
    expect(status.review).toBe(1);
    expect(status.errors).toBe(1);
    expect(status.stored).toBe(0);
    expect(status.results[0]).toMatchObject({
      sequence: 2,
      category: "weapon",
      imagePath: absolutePath,
      parsed: false,
      needsReview: true,
      error: "Native post-capture processing for category 'weapons' is not implemented yet; IK catalog is available only.",
    });
    expect(scanResults[0]).toMatchObject({
      category: "weapon",
      extractionStatus: "error",
      valueStatus: "review",
      needsReview: true,
      error: "Native post-capture processing for category 'weapons' is not implemented yet; IK catalog is available only.",
    });
    expect(buildCalls).toBe(0);
  });

  it("persists safe parsed artifacts only when persistence is explicitly enabled", async () => {
    const runDir = await makeRunDir();
    const { relativePath } = await writeCrop(runDir);
    await writeJobs(runDir, [{ sequence: 1, relativePath }]);
    const savedRecords: StoredArtifactRecord[] = [];

    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => safePlumeCapture(),
      saveArtifacts: async (records) => {
        savedRecords.push(...records);
        return { added: records.length, updated: 0 };
      },
    });

    const dryRun = await service.processRun({ persist: false });
    const persistedRun = await service.processRun({ persist: true });

    expect(dryRun.stored).toBe(0);
    expect(dryRun.results[0].persisted).toBe(false);
    expect(persistedRun.stored).toBe(1);
    expect(persistedRun.results[0].persisted).toBe(true);
    expect(persistedRun.scanResultsPath).toBe(path.join(runDir, "scan-results.json"));
    expect(savedRecords).toHaveLength(1);
    expect(savedRecords[0]).toMatchObject({
      name: "Pristine Plume of the Blessed",
      slot: "Plume of Death",
      setName: "Silken Moon's Serenade",
      mainStat: "ATK",
      mainValue: "311",
      source: "native-ik-scan",
      needsReview: false,
    });

    const scanResults = JSON.parse(await fs.readFile(persistedRun.scanResultsPath, "utf8"));
    expect(scanResults[0]).toMatchObject({
      extractionStatus: "parsed",
      valueStatus: "deferred",
      artifactRecordId: savedRecords[0].id,
      persistedArtifact: true,
    });
  });

  it("preserves the native capture timestamp in durable scan results", async () => {
    const runDir = await makeRunDir();
    const { relativePath } = await writeCrop(runDir);
    const capturedAt = "2026-07-09T22:30:50.5886316+02:00";
    await writeJobs(runDir, [{ sequence: 1, relativePath, capturedAt }]);
    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => safePlumeCapture(),
      saveArtifacts: async () => ({ added: 0, updated: 0 }),
    });

    const status = await service.processRun({ persist: false });
    const scanResults = JSON.parse(await fs.readFile(status.scanResultsPath, "utf8"));

    expect(scanResults[0].capturedAt).toBe(capturedAt);
  });

  it("promotes only explicitly selected safe results and writes a durable log", async () => {
    const runDir = await makeRunDir();
    const safeResult: StoredScanResultEntry = {
      id: "safe-result",
      runId: "run-1",
      sequence: 1,
      category: "artifact",
      source: "native-ik-scan",
      imagePath: "artifact-0001.png",
      capturedAt: "2026-07-09T22:30:50.000Z",
      extractionStatus: "parsed",
      extractionConfidence: 96,
      needsReview: false,
      valueStatus: "deferred",
      valueScore: null,
      persistedArtifact: false,
      notes: [],
      artifact: {
        name: "Pristine Plume of the Blessed",
        slot: "Plume of Death",
        level: 20,
        setName: "Silken Moon's Serenade",
        mainStat: "ATK",
        mainValue: "311",
        substats: ["CRIT DMG+7.0%", "DEF+30.6%", "Elemental Mastery+40", "ATK%+5.8%"],
        equipped: "Aino",
      },
      ikMatch: {
        matched: true,
        confidence: 100,
        source: "ik-inventorylists",
        setGood: "SilkenMoonsSerenade",
        artifactGood: "PristinePlumeOfTheBlessed",
        notes: [],
      },
      fieldConfidences: [],
    };
    await fs.writeFile(path.join(runDir, "scan-results.json"), JSON.stringify([safeResult], null, 2), "utf8");
    const savedRecords: StoredArtifactRecord[] = [];
    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => safePlumeCapture(),
      loadArtifacts: async () => ({ ok: true, artifacts: [], total: 0, path: path.join(runDir, "artifact-store.json") }),
      saveArtifacts: async (records) => {
        savedRecords.push(...records);
        return { ok: true, added: records.length, updated: 0, total: records.length, path: path.join(runDir, "artifact-store.json") };
      },
    });

    const status = await service.promoteResults({ resultIds: [safeResult.id] });
    const updatedResults = JSON.parse(await fs.readFile(path.join(runDir, "scan-results.json"), "utf8"));
    const logLines = (await fs.readFile(path.join(runDir, "promotion-log.jsonl"), "utf8")).trim().split(/\r?\n/);

    expect(status).toMatchObject({ ok: true, requested: 1, selected: 1, promoted: 1, added: 1, updated: 0 });
    expect(savedRecords).toHaveLength(1);
    expect(savedRecords[0]).toMatchObject({ source: "native-ik-scan", needsReview: false });
    expect(updatedResults[0]).toMatchObject({ persistedArtifact: true, artifactRecordId: savedRecords[0].id });
    expect(logLines).toHaveLength(1);
  });

  it("approves a corrected review result only after structural and IK validation", async () => {
    const runDir = await makeRunDir();
    const reviewResult: StoredScanResultEntry = {
      id: "review-result",
      runId: "run-review",
      sequence: 6,
      category: "artifact",
      source: "native-ik-scan",
      imagePath: path.join(runDir, "artifact-0006.png"),
      capturedAt: "2026-07-09T22:34:43.000Z",
      extractionStatus: "review",
      extractionConfidence: 96,
      needsReview: true,
      valueStatus: "review",
      valueScore: null,
      persistedArtifact: false,
      notes: ["Substat value has no valid roll combination: DEF+1."],
      artifact: {
        name: "Pristine Plume of the Blessed",
        slot: "Plume of Death",
        level: 20,
        setName: "Silken Moon's Serenade",
        mainStat: "ATK",
        mainValue: "311",
        substats: ["CRIT DMG+7.0%", "Elemental Mastery+40", "ATK%+5.8%", "DEF+1"],
        equipped: "Aino",
      },
    };
    await fs.writeFile(path.join(runDir, "scan-results.json"), JSON.stringify([reviewResult], null, 2), "utf8");
    await fs.writeFile(path.join(runDir, "processing-report.json"), JSON.stringify({
      results: [{ sequence: 6, ocr: [{ id: "artifact-substats", label: "Substats", text: "DEF+1", confidence: 80 }] }],
    }), "utf8");
    const evalSamples: unknown[] = [];
    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => safePlumeCapture(),
      saveArtifacts: async () => ({ added: 0, updated: 0 }),
      loadIkArtifactCatalog: async () => safePlumeIkCatalog(),
      saveReviewSample: async (sample) => {
        evalSamples.push(sample);
        return { ok: true };
      },
    });

    const invalidRarity = await service.reviewResult({
      resultId: reviewResult.id,
      action: "approve",
      artifact: {
        ...reviewResult.artifact!,
        level: 20,
        substats: ["CRIT Rate+2.3%", "Elemental Mastery+40", "ATK%+5.8%", "DEF+23"],
      },
    });
    expect(invalidRarity).toMatchObject({ ok: false, action: "approve" });
    expect(invalidRarity.error).toContain("Implausible substats: CRIT Rate+2.3%");

    const status = await service.reviewResult({
      resultId: reviewResult.id,
      action: "approve",
      note: "Crop confirms DEF+23.",
      artifact: {
        ...reviewResult.artifact!,
        level: 20,
        substats: ["CRIT DMG+7.0%", "Elemental Mastery+40", "ATK%+5.8%", "DEF+23"],
      },
    });
    const updated = JSON.parse(await fs.readFile(path.join(runDir, "scan-results.json"), "utf8"));
    const log = await fs.readFile(path.join(runDir, "review-log.jsonl"), "utf8");

    expect(status).toMatchObject({ ok: true, action: "approve", evalSampleSaved: true, correctedFields: ["substats"] });
    expect(updated[0]).toMatchObject({ extractionStatus: "parsed", needsReview: false, valueStatus: "deferred" });
    expect(updated[0].artifact.substats).toContain("DEF+23");
    expect(updated[0].review).toMatchObject({ status: "approved", correctedFields: ["substats"] });
    expect(evalSamples).toHaveLength(1);
    expect(log).toContain("review-result");
  });

  it("keeps rejected review results blocked from promotion", async () => {
    const runDir = await makeRunDir();
    const result = {
      id: "reject-result",
      runId: "run-review",
      sequence: 7,
      category: "artifact",
      source: "native-ik-scan",
      imagePath: "artifact-0007.png",
      capturedAt: "2026-07-09T22:34:44.000Z",
      extractionStatus: "review",
      extractionConfidence: 70,
      needsReview: true,
      valueStatus: "review",
      valueScore: null,
      persistedArtifact: false,
      notes: [],
    } satisfies StoredScanResultEntry;
    await fs.writeFile(path.join(runDir, "scan-results.json"), JSON.stringify([result], null, 2), "utf8");
    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => safePlumeCapture(),
      saveArtifacts: async () => ({ added: 0, updated: 0 }),
    });

    const status = await service.reviewResult({ resultId: result.id, action: "reject", note: "Crop unreadable." });
    const updated = JSON.parse(await fs.readFile(path.join(runDir, "scan-results.json"), "utf8"));

    expect(status).toMatchObject({ ok: true, action: "reject", evalSampleSaved: false });
    expect(updated[0]).toMatchObject({ extractionStatus: "review", needsReview: true, valueStatus: "review" });
    expect(updated[0].review).toMatchObject({ status: "rejected" });
  });

  it("uses IK inventorylists matching to force review on catalog conflicts", async () => {
    const runDir = await makeRunDir();
    const { relativePath } = await writeCrop(runDir);
    await writeJobs(runDir, [{ sequence: 1, relativePath }]);
    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => safePlumeCapture(),
      saveArtifacts: async () => ({ added: 0, updated: 0 }),
      loadIkArtifactCatalog: async () => safePlumeIkCatalog({ slot: "flower" }),
    });

    const status = await service.processRun({ persist: true });
    const scanResults = JSON.parse(await fs.readFile(status.scanResultsPath, "utf8"));

    expect(status.review).toBe(1);
    expect(status.stored).toBe(0);
    expect(status.results[0].ikMatch).toMatchObject({
      matched: false,
      source: "ik-inventorylists",
      artifactGood: "PristinePlumeOfTheBlessed",
      slotKey: "flower",
    });
    expect(status.results[0].notes?.join(" ")).toContain("IK slot mismatch");
    expect(scanResults[0]).toMatchObject({
      extractionStatus: "review",
      valueStatus: "review",
      persistedArtifact: false,
      ikMatch: {
        matched: false,
        artifactGood: "PristinePlumeOfTheBlessed",
      },
    });
  });

  it("processes native crops through a bounded post-capture queue while preserving result order", async () => {
    const runDir = await makeRunDir();
    const jobs: NativeCaptureJobPayload[] = [];
    for (let sequence = 1; sequence <= 5; sequence++) {
      const { relativePath } = await writeCrop(runDir, `cards/artifact-${sequence}.png`);
      jobs.push({ sequence, relativePath });
    }
    await writeJobs(runDir, jobs);
    let active = 0;
    let maxActive = 0;
    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => {
        active += 1;
        maxActive = Math.max(maxActive, active);
        await new Promise((resolve) => setTimeout(resolve, 10));
        active -= 1;
        return safePlumeCapture();
      },
      saveArtifacts: async () => ({ added: 0, updated: 0 }),
    });

    const status = await service.processRun();

    expect(status.queueConcurrency).toBe(4);
    expect(maxActive).toBeGreaterThan(1);
    expect(status.results.map((result) => result.sequence)).toEqual([1, 2, 3, 4, 5]);
  });

  it("keeps review-only parsed artifacts out of the persistent store", async () => {
    const runDir = await makeRunDir();
    const { relativePath } = await writeCrop(runDir);
    await writeJobs(runDir, [{ sequence: 1, relativePath }]);
    const savedRecords: StoredArtifactRecord[] = [];

    const service = createNativeScannerProcessingService({
      resolveRunDir: () => runDir,
      buildCaptureResult: async () => reviewOnlyCapture(),
      saveArtifacts: async (records) => {
        savedRecords.push(...records);
        return { added: records.length, updated: 0 };
      },
    });

    const status = await service.processRun({ persist: true });

    expect(status.processed).toBe(1);
    expect(status.parsed).toBe(1);
    expect(status.review).toBe(1);
    expect(status.stored).toBe(0);
    expect(status.results[0]).toMatchObject({
      parsed: true,
      needsReview: true,
      persisted: false,
      slot: "Sands of Eon",
    });
    const scanResults = JSON.parse(await fs.readFile(status.scanResultsPath, "utf8"));
    expect(scanResults[0]).toMatchObject({
      extractionStatus: "review",
      valueStatus: "review",
      persistedArtifact: false,
      artifact: {
        slot: "Sands of Eon",
      },
    });
    expect(savedRecords).toHaveLength(0);
  });
});
