/**
 * @file safe-json.test.ts
 * @description Tests for safe JSON parsing utilities with runtime type validation
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import {
  safeJsonParse,
  isMessageArray,
  isChatOverlayState,
  isLayoutConfigRecord,
} from "./safe-json";

describe("safeJsonParse", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  it("should return parsed data when JSON is valid and passes validation", () => {
    const json = '{"key": "value"}';
    const validator = (data: unknown): data is { key: string } =>
      typeof data === "object" && data !== null && "key" in data;

    const result = safeJsonParse(json, validator, { key: "fallback" });
    expect(result).toEqual({ key: "value" });
  });

  it("should return fallback when JSON is invalid", () => {
    const json = "not valid json {{{";
    const validator = (data: unknown): data is string => typeof data === "string";
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);

    const result = safeJsonParse(json, validator, "fallback");
    expect(result).toBe("fallback");
    expect(warnSpy).toHaveBeenCalledWith("safeJsonParse: failed to parse JSON, returning fallback");
  });

  it("should return fallback when parsed data fails validation", () => {
    const json = '{"wrong": "shape"}';
    const validator = (data: unknown): data is { expected: string } =>
      typeof data === "object" && data !== null && "expected" in data;
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);

    const result = safeJsonParse(json, validator, { expected: "default" });
    expect(result).toEqual({ expected: "default" });
    expect(warnSpy).toHaveBeenCalledWith(
      "safeJsonParse: parsed data failed validation, returning fallback"
    );
  });

  it("should return fallback for empty string", () => {
    const validator = (data: unknown): data is string => typeof data === "string";
    vi.spyOn(console, "warn").mockImplementation(() => undefined);

    const result = safeJsonParse("", validator, "fallback");
    expect(result).toBe("fallback");
  });

  it("should handle null JSON value gracefully", () => {
    const validator = (data: unknown): data is object => typeof data === "object" && data !== null;
    vi.spyOn(console, "warn").mockImplementation(() => undefined);

    const result = safeJsonParse("null", validator, {});
    expect(result).toEqual({});
  });
});

describe("isMessageArray", () => {
  it("should return true for a valid message array", () => {
    const messages = [
      {
        id: "msg-1",
        role: "user",
        content: "Hello",
        createdAt: "2026-01-01T00:00:00Z",
      },
      {
        id: "msg-2",
        role: "assistant",
        content: "Hi there!",
        createdAt: "2026-01-01T00:00:01Z",
      },
    ];

    expect(isMessageArray(messages)).toBe(true);
  });

  it("should return true for an empty array", () => {
    expect(isMessageArray([])).toBe(true);
  });

  it("should return true for messages with optional fields", () => {
    const messages = [
      {
        id: "msg-1",
        role: "assistant",
        content: "Response",
        createdAt: "2026-01-01T00:00:00Z",
        model: "llama3.2",
        thinking: "Let me think...",
        promptTokens: 10,
        completionTokens: 5,
        totalTokens: 15,
      },
    ];

    expect(isMessageArray(messages)).toBe(true);
  });

  it("should return false for non-array values", () => {
    expect(isMessageArray(null)).toBe(false);
    expect(isMessageArray(undefined)).toBe(false);
    expect(isMessageArray("string")).toBe(false);
    expect(isMessageArray(42)).toBe(false);
    expect(isMessageArray({})).toBe(false);
  });

  it("should return false when message is missing required fields", () => {
    // Missing id
    expect(isMessageArray([{ role: "user", content: "Hello", createdAt: "2026-01-01" }])).toBe(
      false
    );

    // Missing role
    expect(isMessageArray([{ id: "1", content: "Hello", createdAt: "2026-01-01" }])).toBe(false);

    // Missing content
    expect(isMessageArray([{ id: "1", role: "user", createdAt: "2026-01-01" }])).toBe(false);

    // Missing createdAt
    expect(isMessageArray([{ id: "1", role: "user", content: "Hello" }])).toBe(false);
  });

  it("should return false when role is not a valid enum value", () => {
    const messages = [
      {
        id: "msg-1",
        role: "invalid-role",
        content: "Hello",
        createdAt: "2026-01-01T00:00:00Z",
      },
    ];

    expect(isMessageArray(messages)).toBe(false);
  });

  it("should return false when fields have wrong types", () => {
    const messages = [
      {
        id: 123, // should be string
        role: "user",
        content: "Hello",
        createdAt: "2026-01-01",
      },
    ];

    expect(isMessageArray(messages)).toBe(false);
  });

  it("should return false for array with mixed valid and invalid messages", () => {
    const messages = [
      { id: "1", role: "user", content: "Hello", createdAt: "2026-01-01" },
      { invalid: "message" },
    ];

    expect(isMessageArray(messages)).toBe(false);
  });
});

describe("isChatOverlayState", () => {
  it("should return true for a valid ChatOverlayState", () => {
    expect(isChatOverlayState({ isOpen: true, isMinimized: false })).toBe(true);
    expect(isChatOverlayState({ isOpen: true, isMinimized: true })).toBe(true);
    expect(isChatOverlayState({ isOpen: false, isMinimized: false })).toBe(true);
  });

  it("should reject invalid state: closed AND minimized (invariant violation)", () => {
    expect(isChatOverlayState({ isOpen: false, isMinimized: true })).toBe(false);
  });

  it("should return false for non-object values", () => {
    expect(isChatOverlayState(null)).toBe(false);
    expect(isChatOverlayState(undefined)).toBe(false);
    expect(isChatOverlayState("string")).toBe(false);
    expect(isChatOverlayState(42)).toBe(false);
    expect(isChatOverlayState([])).toBe(false);
  });

  it("should return false when fields are missing", () => {
    expect(isChatOverlayState({ isOpen: true })).toBe(false);
    expect(isChatOverlayState({ isMinimized: false })).toBe(false);
    expect(isChatOverlayState({})).toBe(false);
  });

  it("should return false when fields have wrong types", () => {
    expect(isChatOverlayState({ isOpen: "true", isMinimized: false })).toBe(false);
    expect(isChatOverlayState({ isOpen: true, isMinimized: 0 })).toBe(false);
    expect(isChatOverlayState({ isOpen: 1, isMinimized: 0 })).toBe(false);
  });

  it("should return true when extra fields are present", () => {
    expect(isChatOverlayState({ isOpen: true, isMinimized: false, extra: "field" })).toBe(true);
  });
});

describe("isLayoutConfigRecord", () => {
  it("should return true for a valid layout config record", () => {
    const record = {
      default: {
        id: "default",
        name: "Default Layout",
        layout: [],
      },
      custom: {
        id: "custom",
        name: "Custom Layout",
        layout: [
          { i: "widget-1", x: 0, y: 0, w: 2, h: 2 },
          { i: "widget-2", x: 2, y: 0, w: 3, h: 1 },
        ],
      },
    };

    expect(isLayoutConfigRecord(record)).toBe(true);
  });

  it("should return true for an empty record", () => {
    expect(isLayoutConfigRecord({})).toBe(true);
  });

  it("should return false for non-object values", () => {
    expect(isLayoutConfigRecord(null)).toBe(false);
    expect(isLayoutConfigRecord(undefined)).toBe(false);
    expect(isLayoutConfigRecord("string")).toBe(false);
    expect(isLayoutConfigRecord(42)).toBe(false);
    expect(isLayoutConfigRecord([])).toBe(false);
  });

  it("should return false when layout config is missing required fields", () => {
    // Missing id
    expect(isLayoutConfigRecord({ layout1: { name: "Test", layout: [] } })).toBe(false);

    // Missing name
    expect(isLayoutConfigRecord({ layout1: { id: "1", layout: [] } })).toBe(false);

    // Missing layout
    expect(isLayoutConfigRecord({ layout1: { id: "1", name: "Test" } })).toBe(false);
  });

  it("should return false when layout array contains invalid widget placements", () => {
    const record = {
      test: {
        id: "test",
        name: "Test",
        layout: [{ i: "widget-1", x: 0 }], // missing y, w, h
      },
    };

    expect(isLayoutConfigRecord(record)).toBe(false);
  });

  it("should return false when widget placement fields have wrong types", () => {
    const record = {
      test: {
        id: "test",
        name: "Test",
        layout: [{ i: 123, x: 0, y: 0, w: 2, h: 2 }], // i should be string
      },
    };

    expect(isLayoutConfigRecord(record)).toBe(false);
  });

  it("should return true with widget placements that have optional fields", () => {
    const record = {
      test: {
        id: "test",
        name: "Test",
        layout: [
          {
            i: "widget-1",
            x: 0,
            y: 0,
            w: 2,
            h: 2,
            minW: 1,
            maxW: 4,
            static: true,
          },
        ],
      },
    };

    expect(isLayoutConfigRecord(record)).toBe(true);
  });
});
