// #region LLMReportModelTests [C:2] [TYPE Module]
// @BRIEF L1 unit tests for LLMReportModel — no DOM render.
// @RELATION BINDS_TO -> [LLMReportModel]
import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("$lib/api.js", () => ({
  api: {
    fetchApi: vi.fn(),
    requestApi: vi.fn().mockResolvedValue({}),
    getTask: vi.fn().mockResolvedValue({ id: "t1", status: "completed" }),
    getTaskLogs: vi.fn().mockResolvedValue([]),
    getStorageFileBlob: vi.fn(),
  },
}));
vi.mock("$lib/cot-logger", () => ({ log: vi.fn() }));

import { LLMReportModel } from "../LLMReportModel.svelte.ts";
import { api } from "$lib/api.js";

// #region LLMReportModel.InvariantTests [C:2] [TYPE Function]
// @BRIEF L1 invariants: initial state, loadReport transitions, error path.
describe("LLMReportModel — L1 invariants", () => {
  let model: LLMReportModel;
  beforeEach(() => { vi.clearAllMocks(); model = new LLMReportModel(); });

  // #region LLMReportModel.InitialState [C:2] [TYPE Test]
  it("starts in idle state", () => {
    expect(model.screenState).toBe("idle");
    expect(model.task).toBeNull();
    expect(model.logs).toEqual([]);
    expect(model.error).toBeNull();
  });
  // #endregion

  // #region LLMReportModel.LoadReport [C:2] [TYPE Test]
  it("loadReport transitions to loaded on success", async () => {
    vi.mocked(api.getTask).mockResolvedValue({ id: "t1", status: "completed" } as any);
    vi.mocked(api.getTaskLogs).mockResolvedValue([]);
    model.taskId = "t1";
    await model.loadReport();
    expect(model.screenState).toBe("loaded");
    expect(model.task?.id).toBe("t1");
  });

  it("loadReport returns early when no taskId", async () => {
    await model.loadReport();
    expect(api.getTask).not.toHaveBeenCalled();
    expect(model.screenState).toBe("idle");
  });

  it("loadReport transitions to error on failure", async () => {
    vi.mocked(api.getTask).mockRejectedValue(new Error("Task not found"));
    model.taskId = "t1";
    await model.loadReport();
    expect(model.screenState).toBe("error");
    expect(model.error).toBe("Task not found");
  });

  it("loadReport stores logs array", async () => {
    vi.mocked(api.getTask).mockResolvedValue({ id: "t1" } as any);
    vi.mocked(api.getTaskLogs).mockResolvedValue([{ level: "info", message: "started" }]);
    model.taskId = "t1";
    await model.loadReport();
    expect(model.logs).toHaveLength(1);
  });

  it("loadReport normalizes non-array logs to empty array", async () => {
    vi.mocked(api.getTask).mockResolvedValue({ id: "t1" } as any);
    vi.mocked(api.getTaskLogs).mockResolvedValue(null);
    model.taskId = "t1";
    await model.loadReport();
    expect(model.logs).toEqual([]);
  });

  it("refresh() delegates to loadReport", () => {
    const spy = vi.spyOn(model, "loadReport").mockResolvedValue();
    model.refresh();
    expect(spy).toHaveBeenCalled();
    spy.mockRestore();
  });
  // #endregion

  // #region LLMReportModel.Screenshots [C:2] [TYPE Test]
  it("loadScreenshotBlobs does nothing with empty paths", async () => {
    await model.loadScreenshotBlobs([]);
    expect(api.getStorageFileBlob).not.toHaveBeenCalled();
  });

  it("loadScreenshotBlobs fetches blobs and creates URLs", async () => {
    const blob = new Blob(["fake"]);
    vi.mocked(api.getStorageFileBlob).mockResolvedValue(blob);
    const createSpy = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:url1");
    await model.loadScreenshotBlobs(["path/to/screen1.png"]);
    expect(api.getStorageFileBlob).toHaveBeenCalledWith("path/to/screen1.png");
    expect(model.screenshotBlobUrls["path/to/screen1.png"]).toBe("blob:url1");
    createSpy.mockRestore();
  });

  it("loadScreenshotBlobs handles blob fetch errors", async () => {
    vi.mocked(api.getStorageFileBlob).mockRejectedValue(new Error("Not found"));
    await model.loadScreenshotBlobs(["bad-path.png"]);
    expect(model.screenshotBlobUrls).toEqual({});
    expect(model.screenshotLoadErrors["bad-path.png"]).toBe("Not found");
  });

  it("loadScreenshotBlobs respects stale token", async () => {
    // Simulate a stale token by calling loadScreenshotBlobs while delaying
    const blob = new Blob(["fake"]);
    const revokeSpy = vi.spyOn(URL, "revokeObjectURL").mockReturnValue();
    const createSpy = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:stale");
    // Make blob fetch return a promise that doesn't resolve until we increment the token
    let resolveBlob: (b: Blob) => void;
    vi.mocked(api.getStorageFileBlob).mockReturnValue(new Promise((r) => { resolveBlob = r; }) as any);
    const promise = model.loadScreenshotBlobs(["stale-path.png"]);
    // Now increment the token to make the in-flight call stale
    model["_screenshotLoadToken"]++;
    // Resolve the blob fetch
    resolveBlob!(blob);
    await promise;
    // Since token changed, blobs should be revoked
    expect(revokeSpy).toHaveBeenCalled();
    revokeSpy.mockRestore();
    createSpy.mockRestore();
  });
  // #endregion

  // #region LLMReportModel.OpenScreenshot [C:2] [TYPE Test]
  it("openScreenshot opens window with blob url", () => {
    const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
    model.screenshotBlobUrls["path.png"] = "blob:url";
    model.openScreenshot("path.png");
    expect(openSpy).toHaveBeenCalledWith("blob:url", "_blank", "noopener,noreferrer");
    openSpy.mockRestore();
  });

  it("openScreenshot does nothing when url missing", () => {
    const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
    model.openScreenshot("missing.png");
    expect(openSpy).not.toHaveBeenCalled();
    openSpy.mockRestore();
  });
  // #endregion

  // #region LLMReportModel.Cleanup [C:2] [TYPE Test]
  it("cleanupBlobs revokes all URLs", () => {
    model.screenshotBlobUrls = { a: "blob:a", b: "blob:b" };
    const revokeSpy = vi.spyOn(URL, "revokeObjectURL").mockReturnValue();
    model.cleanupBlobs();
    expect(revokeSpy).toHaveBeenCalledTimes(2);
    expect(model.screenshotBlobUrls).toEqual({});
    revokeSpy.mockRestore();
  });
  // #endregion
});
// #endregion

// #region LLMReportModel.DerivedState [C:2] [TYPE Function]
// @BRIEF Derived state correctness: result, checkResult, timings, screenshotPaths, sentLogs.
describe("LLMReportModel — Derived State", () => {
  let model: LLMReportModel;
  beforeEach(() => { vi.clearAllMocks(); model = new LLMReportModel(); });

  it("result defaults to empty object", () => {
    expect(model.result).toEqual({});
  });

  it("result mirrors task.result", () => {
    model.task = { id: "t1", result: { status: "PASS", summary: "All good" } as any };
    expect(model.result.status).toBe("PASS");
  });

  it("checkResult returns FAIL for FAIL status", () => {
    model.task = { id: "t1", result: { status: "FAIL" } as any };
    expect(model.checkResult.level).toBe("fail");
  });

  it("checkResult returns WARN for WARN status", () => {
    model.task = { id: "t1", result: { status: "WARN" } as any };
    expect(model.checkResult.level).toBe("warn");
  });

  it("checkResult returns PASS for PASS status", () => {
    model.task = { id: "t1", result: { status: "PASS" } as any };
    expect(model.checkResult.level).toBe("pass");
  });

  it("checkResult returns UNKNOWN for missing status", () => {
    model.task = { id: "t1", result: {} as any };
    expect(model.checkResult.level).toBe("unknown");
  });

  it("timings returns empty object when no timings", () => {
    expect(model.timings).toEqual({});
  });

  it("timings returns validation_duration_ms", () => {
    model.task = { id: "t1", result: { timings: { validation_duration_ms: 1500 } } as any };
    expect(model.timings.validation_duration_ms).toBe(1500);
  });

  it("screenshotPaths uses screenshot_paths array", () => {
    model.task = { id: "t1", result: { screenshot_paths: ["a.png", "b.png"] } as any };
    expect(model.screenshotPaths).toEqual(["a.png", "b.png"]);
  });

  it("screenshotPaths falls back to single screenshot_path", () => {
    model.task = { id: "t1", result: { screenshot_path: "single.png" } as any };
    expect(model.screenshotPaths).toEqual(["single.png"]);
  });

  it("screenshotPaths returns empty when none present", () => {
    expect(model.screenshotPaths).toEqual([]);
  });

  it("sentLogs returns logs_sent_to_llm array", () => {
    model.task = { id: "t1", result: { logs_sent_to_llm: ["log1", "log2"] } as any };
    expect(model.sentLogs).toEqual(["log1", "log2"]);
  });

  it("sentLogs returns empty array when missing", () => {
    expect(model.sentLogs).toEqual([]);
  });
});
// #endregion

// #region LLMReportModel.StaticUtils [C:2] [TYPE Function]
// @BRIEF Static utility method tests.
describe("LLMReportModel — Static Utils", () => {
  // #region LLMReportModel.FormatDate [C:2] [TYPE Test]
  describe("formatDate", () => {
    it("returns '-' for null", () => {
      expect(LLMReportModel.formatDate(null)).toBe("-");
    });

    it("returns '-' for invalid date", () => {
      expect(LLMReportModel.formatDate("not-a-date")).toBe("-");
    });

    it("formats valid date string", () => {
      const result = LLMReportModel.formatDate("2026-06-01T12:00:00Z");
      expect(result).toContain("2026");
    });
  });
  // #endregion

  // #region LLMReportModel.FormatMs [C:2] [TYPE Test]
  describe("formatMs", () => {
    it("returns '-' for NaN", () => {
      expect(LLMReportModel.formatMs("abc")).toBe("-");
    });

    it("returns '-' for negative", () => {
      expect(LLMReportModel.formatMs(-100)).toBe("-");
    });

    it("formats ms when < 1000", () => {
      expect(LLMReportModel.formatMs(500)).toBe("500 ms");
    });

    it("formats seconds when >= 1000", () => {
      expect(LLMReportModel.formatMs(2500)).toBe("2.50 s");
    });
  });
  // #endregion

  // #region LLMReportModel.GetCheckResultClasses [C:2] [TYPE Test]
  describe("getCheckResultClasses", () => {
    it("returns fail classes", () => {
      const cls = LLMReportModel.getCheckResultClasses("fail");
      expect(cls).toContain("destructive");
    });

    it("returns warn classes", () => {
      const cls = LLMReportModel.getCheckResultClasses("warn");
      expect(cls).toContain("warning");
    });

    it("returns pass classes", () => {
      const cls = LLMReportModel.getCheckResultClasses("pass");
      expect(cls).toContain("success");
    });

    it("returns muted for unknown level", () => {
      const cls = LLMReportModel.getCheckResultClasses("unknown" as any);
      expect(cls).toContain("muted");
    });
  });
  // #endregion
});
// #endregion
