import { describe, expect, it } from "vitest";
import { detailFingerprint, fingerprintDataUrl, isRepeatedProcessedPageFingerprint, screenFingerprint } from "./autoScanLoop";
import { runAutoScanLoop } from "./autoScanLoop";
import type { AutoScanLoopDependencies } from "./autoScanLoop";
import type { CaptureResult } from "../types/global";

type ScanTestCapture = CaptureResult & { inventoryGrid: NonNullable<CaptureResult["inventoryGrid"]> };

const sampleGrid = {
  centers: [{ x: 80, y: 90, row: 0, col: 0 }],
  rows: 1,
  cols: 1,
  confidence: 86,
  source: "detected" as const,
};

const sampleParse = {
  name: "A Tiara of Torrents",
  slot: "Flower of Life",
  level: 16,
  mainStat: "HP",
  mainValue: "10.7%",
  substats: ["ATK+29", "DEF+10"],
  setName: "Tenacity of the Millelith",
  equipped: "Traveler",
  confidence: 84,
  notes: [],
  fields: {
    name: { value: "A Tiara of Torrents", confidence: 84, source: "ocr" as const },
    slot: { value: "Flower of Life", confidence: 84, source: "ocr" as const },
    level: { value: "16", confidence: 84, source: "ocr" as const },
    mainStat: { value: "HP", confidence: 84, source: "ocr" as const },
    mainValue: { value: "10.7%", confidence: 84, source: "ocr" as const },
    setName: { value: "Tenacity of the Millelith", confidence: 84, source: "ocr" as const },
    equipped: { value: "Traveler", confidence: 84, source: "ocr" as const },
    substats: { value: "ATK+29, DEF+10", confidence: 84, source: "ocr" as const },
  },
};

function capture(overrides: Partial<ScanTestCapture> = {}): ScanTestCapture {
  return {
    id: "source",
    name: "screen-capture",
    width: 1920,
    height: 1080,
    dataUrl: "data:image/png;base64,AA",
    capturedAt: "2026-01-01T00:00:00.000Z",
    captureTarget: "desktop-source",
    ocr: [],
    detailDataUrl: "data:image/png;base64,DETAIL-AAA",
    inventoryDataUrl: "data:image/png;base64,GRID-AAA",
    inventoryGrid: sampleGrid,
    artifactDetail: { present: true, confidence: 84, orangeHits: 12, greenHits: 8, textHits: 28 },
    ...overrides,
  };
}

describe("autoScanLoop fingerprints", () => {
  it("distinguishes captures that share the same prefix but differ later", () => {
    const sharedPrefix = "data:image/png;base64," + "A".repeat(180);
    const left = sharedPrefix + "LEFT-" + "B".repeat(600);
    const right = sharedPrefix + "RIGHT-" + "C".repeat(600);

    expect(fingerprintDataUrl(left)).not.toBe(fingerprintDataUrl(right));
  });

  it("stays stable for the same capture string", () => {
    const dataUrl = "data:image/png;base64," + "Q".repeat(2048);
    expect(fingerprintDataUrl(dataUrl)).toBe(fingerprintDataUrl(dataUrl));
  });

  it("uses the detail preview for detail-change verification instead of the full frame", () => {
    const captureA = {
      detailDataUrl: "data:image/png;base64," + "DETAIL-A".repeat(64),
      dataUrl: "data:image/png;base64," + "FULL-A".repeat(256),
    } as const;
    const captureB = {
      detailDataUrl: captureA.detailDataUrl,
      dataUrl: "data:image/png;base64," + "FULL-B".repeat(256),
    } as const;

    expect(detailFingerprint(captureA as never)).toBe(detailFingerprint(captureB as never));
  });

  it("prefers native detail fingerprints when preview images are omitted", () => {
    expect(detailFingerprint({ detailFingerprint: "native-detail", dataUrl: "data:image/png;base64,FULL" } as never)).toBe("native-detail");
    expect(screenFingerprint({ inventoryFingerprint: "native-inventory", dataUrl: "data:image/png;base64,FULL" } as never)).toBe("native-inventory");
  });

  it("uses the inventory preview for scroll verification when available", () => {
    const captureA = {
      inventoryDataUrl: "data:image/png;base64," + "GRID-A".repeat(64),
      dataUrl: "data:image/png;base64," + "FRAME-A".repeat(256),
    } as const;
    const captureB = {
      inventoryDataUrl: "data:image/png;base64," + "GRID-B".repeat(64),
      dataUrl: captureA.dataUrl,
    } as const;

    expect(screenFingerprint(captureA as never)).not.toBe(screenFingerprint(captureB as never));
  });

  it("treats repeated page fingerprints as a loop only after page one", () => {
    const seen = new Set(["abc"]);
    expect(isRepeatedProcessedPageFingerprint("abc", seen, 1)).toBe(false);
    expect(isRepeatedProcessedPageFingerprint("abc", seen, 2)).toBe(true);
    expect(isRepeatedProcessedPageFingerprint("", seen, 3)).toBe(false);
  });

  it("blocks before first click when start capture is from the primary screen", async () => {
    const startCapture = capture({
      captureTarget: "primary-screen",
      detailDataUrl: `data:image/png;base64,${"D".repeat(600)}`,
    });

    let clicked = 0;
    const deps: AutoScanLoopDependencies = {
      api: {
        clickScreen: async () => {
          clicked += 1;
          return {
            ok: true,
            x: 0,
            y: 0,
            cursorX: 0,
            cursorY: 0,
            clicked: true,
            moved: true,
            focused: true,
          };
        },
        scrollScreen: async () => ({ ok: true, notchesSent: 0 }),
        getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }),
      },
      captureSelectedSource: async () => capture({ detailDataUrl: `data:image/png;base64,${"E".repeat(600)}` }),
      captureFastSelectedSource: async () => (clicked > 0 ? capture({ detailDataUrl: `data:image/png;base64,${"E".repeat(600)}` }) : startCapture),
      parseArtifact: () => sampleParse,
      persistParsedArtifact: async () => false,
      saveReviewSample: async () => ({ ok: true }),
      getAutoReviewReason: () => "",
      shouldFlagArtifactForReview: () => false,
      appendAutomationLog: () => undefined,
      appendClickDiagnostics: () => undefined,
      setReviewStatus: () => undefined,
      setAutoScanStats: () => undefined,
      shouldStop: () => false,
    };

    const result = await runAutoScanLoop(deps, { scanLimit: 1, skipRows: 0, detectedInventoryCount: null });
    expect(result.blockedReason).toContain("Primary Screen");
    expect(result.status).toBe("blocked");
    expect(clicked).toBe(0);
  });

  it("blocks before first click when no artifact detail card is visible", async () => {
    let clicked = 0;
    const deps: AutoScanLoopDependencies = {
      api: {
        clickScreen: async () => {
          clicked += 1;
          return {
            ok: true,
            x: 0,
            y: 0,
            clicked: true,
            moved: true,
            focused: true,
          };
        },
        scrollScreen: async () => ({ ok: true, notchesSent: 0 }),
        getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }),
      },
      captureSelectedSource: async () => capture(),
      captureFastSelectedSource: async () => capture({ artifactDetail: { present: false, confidence: 10, orangeHits: 0, greenHits: 0, textHits: 3 } }),
      parseArtifact: () => sampleParse,
      persistParsedArtifact: async () => false,
      saveReviewSample: async () => ({ ok: true }),
      getAutoReviewReason: () => "",
      shouldFlagArtifactForReview: () => false,
      appendAutomationLog: () => undefined,
      appendClickDiagnostics: () => undefined,
      setReviewStatus: () => undefined,
      setAutoScanStats: () => undefined,
      shouldStop: () => false,
    };

    const result = await runAutoScanLoop(deps, { scanLimit: 1, skipRows: 0, detectedInventoryCount: null });
    expect(result.blockedReason).toContain("Keine Artifact-Detailansicht");
    expect(result.status).toBe("blocked");
    expect(clicked).toBe(0);
  });

  it("continues when detail verification proves a helper-reported click miss still changed selection", async () => {
    const startCapture = capture({
      detailDataUrl: `data:image/png;base64,${"D".repeat(600)}`,
    });

    let clicked = 0;
    const selectedCaptureOptions: unknown[] = [];
    const selectedFocusFlags: boolean[] = [];
    const fastFocusFlags: boolean[] = [];
    const deps: AutoScanLoopDependencies = {
      api: {
        clickScreen: async () => {
          clicked += 1;
          if (clicked === 1) {
            return {
              ok: true,
              x: 80,
              y: 90,
              cursorX: 960,
              cursorY: 539,
              clicked: false,
              moved: false,
              focused: true,
              isElevated: true,
            };
          }
          return {
            ok: true,
            x: 80,
            y: 90,
            cursorX: 80,
            cursorY: 90,
            clicked: true,
            moved: true,
            focused: true,
            isElevated: true,
          };
        },
        scrollScreen: async () => ({ ok: true, notchesSent: 0 }),
        getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }),
      },
      captureSelectedSource: async (_delayMs, focusGenshin, options) => {
        selectedFocusFlags.push(Boolean(focusGenshin));
        selectedCaptureOptions.push(options);
        return capture({ detailDataUrl: `data:image/png;base64,${"E".repeat(600)}` });
      },
      captureFastSelectedSource: async (_delayMs, focusGenshin) => {
        fastFocusFlags.push(Boolean(focusGenshin));
        return clicked > 0 ? capture({ detailDataUrl: `data:image/png;base64,${"E".repeat(600)}` }) : startCapture;
      },
      parseArtifact: () => sampleParse,
      persistParsedArtifact: async () => true,
      saveReviewSample: async () => ({ ok: true }),
      getAutoReviewReason: () => "",
      shouldFlagArtifactForReview: () => false,
      appendAutomationLog: () => undefined,
      appendClickDiagnostics: () => undefined,
      setReviewStatus: () => undefined,
      setAutoScanStats: () => undefined,
      shouldStop: () => false,
    };

    const result = await runAutoScanLoop(deps, { scanLimit: 1, skipRows: 0, detectedInventoryCount: null });
    expect(result.blockedReason).toBe("");
    expect(result.status).toBe("done");
    expect(clicked).toBe(1);
    expect(result.stats.verified).toBe(1);
    expect(result.stats.parsed).toBe(1);
    expect(selectedFocusFlags).toEqual([false]);
    expect(fastFocusFlags.every((flag) => flag === false)).toBe(true);
    expect(selectedCaptureOptions).toContainEqual({
      ocrMode: "artifact",
      ocrProfile: "fast",
      omitFullFrame: true,
      omitDetailPreview: true,
      omitInventoryPreview: true,
      omitCropImages: true,
      skipOcrUnlessArtifactDetail: true,
    });
  });

  it("processes an initial Paimon-selected artifact before clicking the next tile", async () => {
    let clicked = 0;
    let persisted = 0;
    const deps: AutoScanLoopDependencies = {
      api: {
        clickScreen: async () => {
          clicked += 1;
          return {
            ok: true,
            x: 80,
            y: 90,
            cursorX: 80,
            cursorY: 90,
            clicked: true,
            moved: true,
            focused: true,
          };
        },
        scrollScreen: async () => ({ ok: true, notchesSent: 0 }),
        getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }),
      },
      captureSelectedSource: async () => capture({ detailDataUrl: `data:image/png;base64,${"I".repeat(600)}` }),
      captureFastSelectedSource: async () => capture({ detailDataUrl: `data:image/png;base64,${"I".repeat(600)}` }),
      parseArtifact: () => sampleParse,
      persistParsedArtifact: async () => {
        persisted += 1;
        return true;
      },
      saveReviewSample: async () => ({ ok: true }),
      getAutoReviewReason: () => "",
      shouldFlagArtifactForReview: () => false,
      appendAutomationLog: () => undefined,
      appendClickDiagnostics: () => undefined,
      setReviewStatus: () => undefined,
      setAutoScanStats: () => undefined,
      shouldStop: () => false,
    };

    const result = await runAutoScanLoop(deps, {
      scanLimit: 1,
      skipRows: 0,
      detectedInventoryCount: null,
      processInitialSelection: true,
    });
    expect(result.status).toBe("done");
    expect(result.stats.parsed).toBe(1);
    expect(result.stats.stored).toBe(1);
    expect(clicked).toBe(0);
    expect(persisted).toBe(1);
  });

  it("starts after the already selected initial tile", async () => {
    let clicked = 0;
    const clickedTargets: Array<{ x: number; y: number }> = [];
    const logs: string[] = [];
    const twoTileGrid = {
      centers: [
        { x: 179, y: 254, row: 0, col: 0 },
        { x: 325, y: 254, row: 0, col: 1 },
      ],
      rows: 1,
      cols: 2,
      confidence: 86,
      source: "detected" as const,
    };
    const deps: AutoScanLoopDependencies = {
      api: {
        clickScreen: async (x, y) => {
          clicked += 1;
          clickedTargets.push({ x, y });
          return {
            ok: true,
            x: 80,
            y: 90,
            cursorX: 80,
            cursorY: 90,
            clicked: true,
            moved: true,
            focused: true,
          };
        },
        scrollScreen: async () => ({ ok: true, notchesSent: 0 }),
        getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }),
      },
      captureSelectedSource: async () => capture({ inventoryGrid: twoTileGrid, detailDataUrl: `data:image/png;base64,${"I".repeat(600)}` }),
      captureFastSelectedSource: async () => capture({ inventoryGrid: twoTileGrid, detailDataUrl: `data:image/png;base64,${"I".repeat(600)}` }),
      parseArtifact: () => sampleParse,
      persistParsedArtifact: async () => true,
      saveReviewSample: async () => ({ ok: true }),
      getAutoReviewReason: () => "",
      shouldFlagArtifactForReview: () => false,
      appendAutomationLog: (line) => logs.push(line),
      appendClickDiagnostics: () => undefined,
      setReviewStatus: () => undefined,
      setAutoScanStats: () => undefined,
      shouldStop: () => false,
    };

    await runAutoScanLoop(deps, {
      scanLimit: 2,
      skipRows: 0,
      detectedInventoryCount: null,
      processInitialSelection: true,
      skipInitialGridTarget: true,
    });

    expect(clicked).toBe(1);
    expect(clickedTargets[0]).toEqual({ x: 325, y: 254 });
    expect(logs.some((line) => line.includes("r0 c0"))).toBe(false);
  });

  it("does not skip the first grid tile when the visible inventory selection was user-made", async () => {
    let clicked = 0;
    const clickedTargets: Array<{ x: number; y: number }> = [];
    const twoTileGrid = {
      centers: [
        { x: 179, y: 254, row: 0, col: 0 },
        { x: 325, y: 254, row: 0, col: 1 },
      ],
      rows: 1,
      cols: 2,
      confidence: 86,
      source: "detected" as const,
    };
    const deps: AutoScanLoopDependencies = {
      api: {
        clickScreen: async (x, y) => {
          clicked += 1;
          clickedTargets.push({ x, y });
          return {
            ok: true,
            x,
            y,
            cursorX: x,
            cursorY: y,
            clicked: true,
            moved: true,
            focused: true,
          };
        },
        scrollScreen: async () => ({ ok: true, notchesSent: 0 }),
        getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }),
      },
      captureSelectedSource: async () => capture({ inventoryGrid: twoTileGrid, detailDataUrl: `data:image/png;base64,${"A".repeat(600)}` }),
      captureFastSelectedSource: async () => (clicked > 0
        ? capture({ inventoryGrid: twoTileGrid, detailDataUrl: `data:image/png;base64,${"A".repeat(600)}` })
        : capture({ inventoryGrid: twoTileGrid, detailDataUrl: `data:image/png;base64,${"I".repeat(600)}` })),
      parseArtifact: () => sampleParse,
      persistParsedArtifact: async () => true,
      saveReviewSample: async () => ({ ok: true }),
      getAutoReviewReason: () => "",
      shouldFlagArtifactForReview: () => false,
      appendAutomationLog: () => undefined,
      appendClickDiagnostics: () => undefined,
      setReviewStatus: () => undefined,
      setAutoScanStats: () => undefined,
      shouldStop: () => false,
    };

    await runAutoScanLoop(deps, {
      scanLimit: 2,
      skipRows: 0,
      detectedInventoryCount: null,
      processInitialSelection: true,
      skipInitialGridTarget: false,
    });

    expect(clickedTargets[0]).toEqual({ x: 179, y: 254 });
  });

  it("polls for the next inventory page after scroll instead of sleeping a fixed settle delay", async () => {
    const started = Date.now();
    let clicked = 0;
    let scrolled = false;
    let parsedIndex = 0;
    const waits: string[] = [];
    const singleTileGrid = {
      centers: [{ x: 179, y: 254, row: 0, col: 0 }],
      rows: 1,
      cols: 1,
      confidence: 86,
      source: "detected" as const,
    };
    const deps: AutoScanLoopDependencies = {
      api: {
        clickScreen: async (x, y) => {
          clicked += 1;
          return {
            ok: true,
            x,
            y,
            cursorX: x,
            cursorY: y,
            clicked: true,
            moved: true,
            focused: true,
          };
        },
        scrollScreen: async () => {
          scrolled = true;
          waits.push("scroll");
          return { ok: true, notchesSent: -9 };
        },
        getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }),
      },
      captureSelectedSource: async () => capture({
        inventoryGrid: singleTileGrid,
        inventoryFingerprint: scrolled ? "page-2" : "page-1",
        detailFingerprint: clicked > 1 ? "detail-2-selected" : "detail-1-selected",
      }),
      captureFastSelectedSource: async () => {
        const pageFingerprint = scrolled ? "page-2" : "page-1";
        const detail = clicked > 1 ? "detail-2" : clicked > 0 ? "detail-1" : "detail-start";
        return capture({
          inventoryGrid: singleTileGrid,
          inventoryFingerprint: pageFingerprint,
          detailFingerprint: `${detail}:${pageFingerprint}`,
        });
      },
      parseArtifact: () => ({
        ...sampleParse,
        name: `Artifact ${++parsedIndex}`,
        fields: {
          ...sampleParse.fields,
          name: { value: `Artifact ${parsedIndex}`, confidence: 84, source: "ocr" as const },
        },
      }),
      persistParsedArtifact: async () => true,
      saveReviewSample: async () => ({ ok: true }),
      getAutoReviewReason: () => "",
      shouldFlagArtifactForReview: () => false,
      appendAutomationLog: () => undefined,
      appendClickDiagnostics: () => undefined,
      setReviewStatus: () => undefined,
      setAutoScanStats: () => undefined,
      shouldStop: () => false,
    };

    const result = await runAutoScanLoop(deps, { scanLimit: 2, skipRows: 0, detectedInventoryCount: null });

    expect(result.status).toBe("done");
    expect(result.stats.parsed).toBe(2);
    expect(waits).toEqual(["scroll"]);
    expect(Date.now() - started).toBeLessThan(700);
  });

  it("queues store writes so the next tile can be clicked before persistence finishes", async () => {
    let clicked = 0;
    let selectedReads = 0;
    const persistResolvers: Array<(value: boolean) => void> = [];
    const twoTileGrid = {
      centers: [
        { x: 179, y: 254, row: 0, col: 0 },
        { x: 325, y: 254, row: 0, col: 1 },
      ],
      rows: 1,
      cols: 2,
      confidence: 86,
      source: "detected" as const,
    };
    const deps: AutoScanLoopDependencies = {
      api: {
        clickScreen: async (x, y) => {
          clicked += 1;
          return {
            ok: true,
            x,
            y,
            cursorX: x,
            cursorY: y,
            clicked: true,
            moved: true,
            focused: true,
          };
        },
        scrollScreen: async () => ({ ok: true, notchesSent: 0 }),
        getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }),
      },
      captureSelectedSource: async () => {
        selectedReads += 1;
        return capture({
          inventoryGrid: twoTileGrid,
          detailFingerprint: `detail-read-${selectedReads}`,
        });
      },
      captureFastSelectedSource: async () => capture({
        inventoryGrid: twoTileGrid,
        detailFingerprint: clicked === 0 ? "detail-start" : `detail-click-${clicked}`,
      }),
      parseArtifact: () => ({
        ...sampleParse,
        name: `Artifact ${selectedReads}`,
        fields: {
          ...sampleParse.fields,
          name: { value: `Artifact ${selectedReads}`, confidence: 84, source: "ocr" as const },
        },
      }),
      persistParsedArtifact: async () => new Promise<boolean>((resolve) => {
        persistResolvers.push(resolve);
      }),
      saveReviewSample: async () => ({ ok: true }),
      getAutoReviewReason: () => "",
      shouldFlagArtifactForReview: () => false,
      appendAutomationLog: () => undefined,
      appendClickDiagnostics: () => undefined,
      setReviewStatus: () => undefined,
      setAutoScanStats: () => undefined,
      shouldStop: () => false,
    };

    const runPromise = runAutoScanLoop(deps, { scanLimit: 2, skipRows: 0, detectedInventoryCount: null });
    await new Promise((resolve) => setTimeout(resolve, 250));

    expect(clicked).toBe(2);
    expect(persistResolvers).toHaveLength(1);

    persistResolvers[0](true);
    for (let attempt = 0; attempt < 10 && persistResolvers.length < 2; attempt += 1) {
      await new Promise((resolve) => setTimeout(resolve, 20));
    }
    expect(persistResolvers).toHaveLength(2);
    persistResolvers[1](true);

    const result = await runPromise;
    expect(result.status).toBe("done");
    expect(result.stats.parsed).toBe(2);
    expect(result.stats.stored).toBe(2);
    expect(result.stats.activeScanMs).toBeGreaterThan(0);
    expect(result.stats.writeFlushMs).toBeGreaterThan(0);
    expect(result.stats.activeScanMs).toBeLessThanOrEqual(result.stats.elapsedMs);
  });
});
