import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { IconSetBundle } from "../services/renderers/types.js";

// ─── Mock LLM inference ─────────────────────────────────────────────────────────

const MOCK_SVG_1 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/></svg>';
const MOCK_SVG_2 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>';
const MOCK_SVG_3 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20"/></svg>';
const MOCK_SVG_4 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 12h14"/></svg>';

const mockLlmIcons = [
  { name: "home", svg: MOCK_SVG_1 },
  { name: "circle", svg: MOCK_SVG_2 },
  { name: "square", svg: MOCK_SVG_3 },
  { name: "line", svg: MOCK_SVG_4 },
];

const mockPuterChatComplete = vi.fn().mockResolvedValue(
  JSON.stringify(mockLlmIcons),
);

vi.mock("../services/puter-inference.js", () => ({
  puterChatComplete: (...args: unknown[]) => mockPuterChatComplete(...args),
}));

// ─── Tests ──────────────────────────────────────────────────────────────────────

describe("validateAndCleanSvg", () => {
  let validateAndCleanSvg: (
    raw: string,
  ) => { svg: string; valid: boolean; error?: string };

  beforeEach(async () => {
    const mod = await import("../services/renderers/icon-renderer.js");
    validateAndCleanSvg = mod.validateAndCleanSvg;
  });

  it("returns valid=true and cleaned SVG for a valid SVG with path element", () => {
    const raw =
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2L2 7"/></svg>';
    const result = validateAndCleanSvg(raw);
    expect(result.valid).toBe(true);
    expect(result.svg).toContain("xmlns");
    expect(result.svg).toContain("viewBox");
  });

  it("adds viewBox='0 0 24 24' when missing", () => {
    const raw =
      '<svg xmlns="http://www.w3.org/2000/svg"><path d="M12 2L2 7"/></svg>';
    const result = validateAndCleanSvg(raw);
    expect(result.valid).toBe(true);
    expect(result.svg).toContain('viewBox="0 0 24 24"');
  });

  it("adds xmlns when missing", () => {
    const raw = '<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5z"/></svg>';
    const result = validateAndCleanSvg(raw);
    expect(result.valid).toBe(true);
    expect(result.svg).toContain("xmlns");
  });

  it("returns valid=false and error for SVG with no drawable elements", () => {
    const raw =
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g></g></svg>';
    const result = validateAndCleanSvg(raw);
    expect(result.valid).toBe(false);
    expect(result.error).toBeTruthy();
  });

  it("returns valid=true for SVG with circle element", () => {
    const raw =
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>';
    const result = validateAndCleanSvg(raw);
    expect(result.valid).toBe(true);
  });

  it("returns valid=true for SVG with rect element", () => {
    const raw =
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="0" y="0" width="24" height="24"/></svg>';
    const result = validateAndCleanSvg(raw);
    expect(result.valid).toBe(true);
  });
});

describe("renderIconSet integration", () => {
  let renderIconSet: (
    input: Record<string, unknown>,
  ) => Promise<import("../services/renderers/types.js").RenderResult>;

  beforeEach(async () => {
    vi.clearAllMocks();
    mockPuterChatComplete.mockResolvedValue(JSON.stringify(mockLlmIcons));
    const mod = await import("../services/renderers/icon-renderer.js");
    renderIconSet = mod.renderIconSet;
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  it("returns a RenderResult with icon-set-bundle JSON structure", async () => {
    const result = await renderIconSet({
      description: "UI navigation icons",
      style: "outline",
      count: 4,
    });

    expect(result.filename).toBe("icon-set-bundle.json");
    expect(result.contentType).toBe("application/json");

    const bundle = JSON.parse(result.buffer.toString()) as IconSetBundle;
    expect(bundle.type).toBe("icon-set-bundle");
    expect(Array.isArray(bundle.icons)).toBe(true);
    expect(bundle.icons.length).toBeGreaterThan(0);
  });

  it("each icon has svgSource and pngs object with 16, 32, 64 keys", async () => {
    const result = await renderIconSet({
      description: "UI navigation icons",
      style: "outline",
      count: 4,
    });

    const bundle = JSON.parse(result.buffer.toString()) as IconSetBundle;
    for (const icon of bundle.icons) {
      expect(typeof icon.name).toBe("string");
      expect(typeof icon.svgSource).toBe("string");
      expect(typeof icon.pngs).toBe("object");
      expect(icon.pngs["16"]).toBeTruthy();
      expect(icon.pngs["32"]).toBeTruthy();
      expect(icon.pngs["64"]).toBeTruthy();
    }
  });

  it("PNG values are base64 encoded strings", async () => {
    const result = await renderIconSet({
      description: "UI navigation icons",
      style: "outline",
      count: 4,
    });

    const bundle = JSON.parse(result.buffer.toString()) as IconSetBundle;
    const icon = bundle.icons[0]!;
    // Base64 strings should only contain valid base64 chars
    expect(icon.pngs["16"]).toMatch(/^[A-Za-z0-9+/=]+$/);
  });

  it("bundle style matches the requested style", async () => {
    const result = await renderIconSet({
      description: "filled icons",
      style: "filled",
      count: 4,
    });

    const bundle = JSON.parse(result.buffer.toString()) as IconSetBundle;
    expect(bundle.style).toBe("filled");
  });

  it("retries once if LLM returns non-JSON and then succeeds", async () => {
    mockPuterChatComplete
      .mockResolvedValueOnce("here are your icons: sorry invalid JSON")
      .mockResolvedValueOnce(JSON.stringify([mockLlmIcons[0]]));

    const result = await renderIconSet({
      description: "single icon",
      style: "outline",
      count: 1,
    });

    expect(mockPuterChatComplete).toHaveBeenCalledTimes(2);
    const bundle = JSON.parse(result.buffer.toString()) as IconSetBundle;
    expect(bundle.icons.length).toBeGreaterThan(0);
  });

  it("uses default style 'outline' and default count 4 when not specified", async () => {
    const result = await renderIconSet({
      description: "generic icons",
    });

    const bundle = JSON.parse(result.buffer.toString()) as IconSetBundle;
    expect(bundle.style).toBe("outline");
  });
});
