import { describe, it, expect, vi, beforeEach } from "vitest";

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------

const mocks = vi.hoisted(() => ({
  readFileSync: vi.fn(),
  existsSync: vi.fn(),
}));

vi.mock("node:fs", () => ({
  readFileSync: mocks.readFileSync,
  existsSync: mocks.existsSync,
}));

// ---------------------------------------------------------------------------
// Import module under test AFTER mocks
// ---------------------------------------------------------------------------

const { loadAndValidateEvals, EvalValidationError } = await import(
  "../schema.js"
);

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

const VALID_EVALS = {
  skill_name: "social-media-posting",
  evals: [
    {
      id: 1,
      name: "Basic post generation",
      prompt: "Create a social media post about AI",
      expected_output: "A well-crafted social media post",
      files: [],
      assertions: [
        { id: "assert-1", text: "Output mentions AI", type: "boolean" },
        { id: "assert-2", text: "Output is under 280 chars", type: "boolean" },
      ],
    },
  ],
};

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("loadAndValidateEvals", () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  it("accepts valid evals.json and returns typed EvalsFile", () => {
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(VALID_EVALS));

    const result = loadAndValidateEvals("/skills/my-skill");

    expect(result.skill_name).toBe("social-media-posting");
    expect(result.evals).toHaveLength(1);
    expect(result.evals[0].assertions).toHaveLength(2);
  });

  it("throws for missing skill_name", () => {
    const invalid = { ...VALID_EVALS, skill_name: undefined };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(invalid));

    expect(() => loadAndValidateEvals("/skills/my-skill")).toThrow(
      EvalValidationError,
    );
    try {
      loadAndValidateEvals("/skills/my-skill");
    } catch (e: any) {
      expect(e.errors.some((err: any) => err.path === "skill_name")).toBe(true);
    }
  });

  it("throws for missing evals array", () => {
    const invalid = { skill_name: "test" };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(invalid));

    expect(() => loadAndValidateEvals("/skills/my-skill")).toThrow(
      EvalValidationError,
    );
    try {
      loadAndValidateEvals("/skills/my-skill");
    } catch (e: any) {
      expect(e.errors.some((err: any) => err.path === "evals")).toBe(true);
    }
  });

  it("throws for missing prompt in eval case", () => {
    const invalid = {
      skill_name: "test",
      evals: [
        {
          id: 1,
          name: "Test",
          expected_output: "output",
          assertions: [{ id: "a1", text: "ok", type: "boolean" }],
        },
      ],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(invalid));

    expect(() => loadAndValidateEvals("/skills/my-skill")).toThrow(
      EvalValidationError,
    );
    try {
      loadAndValidateEvals("/skills/my-skill");
    } catch (e: any) {
      expect(
        e.errors.some((err: any) => err.path === "evals[0].prompt"),
      ).toBe(true);
    }
  });

  it("throws for missing assertions", () => {
    const invalid = {
      skill_name: "test",
      evals: [
        {
          id: 1,
          name: "Test",
          prompt: "do something",
          expected_output: "output",
        },
      ],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(invalid));

    expect(() => loadAndValidateEvals("/skills/my-skill")).toThrow(
      EvalValidationError,
    );
    try {
      loadAndValidateEvals("/skills/my-skill");
    } catch (e: any) {
      expect(
        e.errors.some((err: any) => err.path === "evals[0].assertions"),
      ).toBe(true);
    }
  });

  it("throws for duplicate assertion IDs within a case", () => {
    const invalid = {
      skill_name: "test",
      evals: [
        {
          id: 1,
          name: "Test",
          prompt: "do something",
          expected_output: "output",
          assertions: [
            { id: "assert-1", text: "first", type: "boolean" },
            { id: "assert-1", text: "duplicate", type: "boolean" },
          ],
        },
      ],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(invalid));

    expect(() => loadAndValidateEvals("/skills/my-skill")).toThrow(
      EvalValidationError,
    );
    try {
      loadAndValidateEvals("/skills/my-skill");
    } catch (e: any) {
      expect(
        e.errors.some((err: any) => err.message.includes("assert-1")),
      ).toBe(true);
    }
  });

  it("throws for invalid JSON with file path info", () => {
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue("{ broken json");

    expect(() => loadAndValidateEvals("/skills/my-skill")).toThrow(
      EvalValidationError,
    );
    try {
      loadAndValidateEvals("/skills/my-skill");
    } catch (e: any) {
      expect(e.errors[0].path).toContain("evals.json");
      expect(e.errors[0].message).toMatch(/parse|JSON/i);
    }
  });

  it("throws for empty assertions array", () => {
    const invalid = {
      skill_name: "test",
      evals: [
        {
          id: 1,
          name: "Test",
          prompt: "do something",
          expected_output: "output",
          assertions: [],
        },
      ],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(invalid));

    expect(() => loadAndValidateEvals("/skills/my-skill")).toThrow(
      EvalValidationError,
    );
    try {
      loadAndValidateEvals("/skills/my-skill");
    } catch (e: any) {
      expect(
        e.errors.some((err: any) => err.message.includes("at least 1")),
      ).toBe(true);
    }
  });

  it("throws when evals.json does not exist", () => {
    mocks.existsSync.mockReturnValue(false);

    expect(() => loadAndValidateEvals("/skills/my-skill")).toThrow(
      EvalValidationError,
    );
    try {
      loadAndValidateEvals("/skills/my-skill");
    } catch (e: any) {
      expect(e.errors[0].message).toMatch(/not found|No evals\.json/i);
    }
  });

  it("migrates Anthropic standard 'expectations' format to assertions with scoped IDs", () => {
    const anthropicFormat = {
      skill_name: "my-skill",
      evals: [
        {
          id: 2,
          name: "Test prompt",
          prompt: "do something",
          expected_output: "output",
          expectations: ["Output includes X", "Output is under 280 chars"],
        },
      ],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(anthropicFormat));

    const result = loadAndValidateEvals("/skills/my-skill");
    expect(result.evals[0].assertions).toHaveLength(2);
    expect(result.evals[0].assertions[0]).toEqual({ id: "2_1", text: "Output includes X", type: "boolean" });
    expect(result.evals[0].assertions[1]).toEqual({ id: "2_2", text: "Output is under 280 chars", type: "boolean" });
  });

  it("auto-fills missing 'name' (Anthropic format) from eval id", () => {
    const anthropicFormatNoName = {
      skill_name: "my-skill",
      evals: [
        {
          id: 5,
          prompt: "do something",
          expected_output: "output",
          expectations: ["Output includes X"],
        },
      ],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(anthropicFormatNoName));

    const result = loadAndValidateEvals("/skills/my-skill");
    expect(result.evals[0].name).toBe("case-5");
    expect(result.evals[0].assertions[0].id).toBe("5_1");
  });

  it("accepts evals with optional files field defaulting to empty array", () => {
    const valid = {
      skill_name: "test",
      evals: [
        {
          id: 1,
          name: "Test",
          prompt: "do something",
          expected_output: "output",
          assertions: [{ id: "a1", text: "check", type: "boolean" }],
        },
      ],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(valid));

    const result = loadAndValidateEvals("/skills/my-skill");
    expect(result.evals[0].files).toEqual([]);
  });

  it("defaults testType to 'unit' when absent", () => {
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(VALID_EVALS));

    const result = loadAndValidateEvals("/skills/my-skill");
    expect(result.evals[0].testType).toBe("unit");
  });

  it("accepts testType 'unit' and passes it through", () => {
    const evalsWithUnit = {
      ...VALID_EVALS,
      evals: [{ ...VALID_EVALS.evals[0], testType: "unit" }],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(evalsWithUnit));

    const result = loadAndValidateEvals("/skills/my-skill");
    expect(result.evals[0].testType).toBe("unit");
  });

  it("accepts testType 'integration' and passes it through", () => {
    const evalsWithIntegration = {
      ...VALID_EVALS,
      evals: [{ ...VALID_EVALS.evals[0], testType: "integration" }],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(evalsWithIntegration));

    const result = loadAndValidateEvals("/skills/my-skill");
    expect(result.evals[0].testType).toBe("integration");
  });

  it("accepts requiredCredentials array and passes it through", () => {
    const evalsWithCreds = {
      ...VALID_EVALS,
      evals: [{
        ...VALID_EVALS.evals[0],
        testType: "integration",
        requiredCredentials: ["OPENAI_API_KEY", "STRIPE_KEY"],
      }],
    };
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(evalsWithCreds));

    const result = loadAndValidateEvals("/skills/my-skill");
    expect(result.evals[0].requiredCredentials).toEqual(["OPENAI_API_KEY", "STRIPE_KEY"]);
    expect(result.evals[0].testType).toBe("integration");
  });

  it("omits requiredCredentials when not provided", () => {
    mocks.existsSync.mockReturnValue(true);
    mocks.readFileSync.mockReturnValue(JSON.stringify(VALID_EVALS));

    const result = loadAndValidateEvals("/skills/my-skill");
    expect(result.evals[0].requiredCredentials).toBeUndefined();
  });
});
