import { describe, expect, test } from "bun:test";

import {
  assertOutputContract,
  validateOutputContract,
  type OutputSchema,
} from "../../src/mcp/output-contract.ts";
import { JSONRPC_VERSION, MCPServer } from "../../src/mcp/index.ts";
import { buildToolTable } from "../../src/mcp/tools.ts";

describe("validateOutputContract", () => {
  test("accepts objects that satisfy required properties", () => {
    const schema: OutputSchema = {
      type: "object",
      required: ["ok"],
      properties: { ok: { type: "boolean" } },
      additionalProperties: false,
    };
    expect(validateOutputContract(schema, { ok: true })).toEqual([]);
  });

  test("reports missing required properties", () => {
    const schema: OutputSchema = {
      type: "object",
      required: ["ok"],
      properties: { ok: { type: "boolean" } },
    };
    expect(validateOutputContract(schema, {})).toEqual(["$: missing required property 'ok'"]);
  });

  test("does not satisfy required properties via prototype chain", () => {
    const schema: OutputSchema = {
      type: "object",
      required: ["ok"],
      properties: { ok: { type: "boolean" } },
      additionalProperties: false,
    };
    const value = Object.create({ ok: true }) as Record<string, unknown>;
    expect(validateOutputContract(schema, value)).toEqual(["$: missing required property 'ok'"]);
  });

  test("reports unexpected properties when additionalProperties is false", () => {
    const schema: OutputSchema = {
      type: "object",
      properties: { ok: { type: "boolean" } },
      additionalProperties: false,
    };
    expect(validateOutputContract(schema, { ok: true, extra: 1 })).toEqual([
      "$: unexpected property 'extra'",
    ]);
  });

  test("treats inherited schema properties as undeclared", () => {
    const schema: OutputSchema = {
      type: "object",
      properties: {},
      additionalProperties: false,
    };
    expect(validateOutputContract(schema, { toString: "owned" })).toEqual([
      "$: unexpected property 'toString'",
    ]);
  });

  test("rejects non-finite numbers for number type", () => {
    const schema: OutputSchema = { type: "number" };
    expect(validateOutputContract(schema, Number.NaN)).toEqual(["$: expected number"]);
    expect(validateOutputContract(schema, Infinity)).toEqual(["$: expected number"]);
    expect(validateOutputContract(schema, -Infinity)).toEqual(["$: expected number"]);
    expect(validateOutputContract(schema, 1.5)).toEqual([]);
  });

  test("validates arrays, items, and enum values", () => {
    const schema: OutputSchema = {
      type: "object",
      required: ["items"],
      properties: {
        items: {
          type: "array",
          items: {
            type: "object",
            required: ["kind"],
            properties: { kind: { type: "string", enum: ["a", "b"] } },
          },
        },
      },
    };
    expect(validateOutputContract(schema, { items: [{ kind: "c" }] })).toEqual([
      "$.items[0].kind: expected one of a, b",
    ]);
  });
});

describe("assertOutputContract", () => {
  test("throws with tool name and validation details", () => {
    const schema: OutputSchema = {
      type: "object",
      required: ["ok"],
      properties: { ok: { type: "boolean" } },
    };
    expect(() => assertOutputContract("demo_tool", schema, { ok: "yes" })).toThrow(
      /demo_tool output contract failed: \$\.ok: expected boolean/,
    );
  });
});

describe("registered output contracts", () => {
  test("covers the first agent-facing structured surfaces", () => {
    const tools = new Map(buildToolTable("full").map((tool) => [tool.name, tool]));
    for (const name of ["brain_context", "brain_pinned_context", "brain_query", "brain_search"]) {
      expect(tools.get(name)?.outputSchema).toBeDefined();
    }
  });

  test("tools/list advertises output schemas for contracted tools", async () => {
    const server = new MCPServer({ vault: "/tmp/o2b-output-contract-test" });
    const response = (await server.handleRequest({
      jsonrpc: JSONRPC_VERSION,
      id: 1,
      method: "tools/list",
    })) as any;
    const tools = new Map(
      (response.result.tools as Array<{ name: string; outputSchema?: unknown }>).map((tool) => [
        tool.name,
        tool,
      ]),
    );
    expect(tools.get("brain_context")?.outputSchema).toBeDefined();
    expect(tools.get("brain_pinned_context")?.outputSchema).toBeDefined();
    expect(tools.get("brain_feedback")?.outputSchema).toBeUndefined();
  });
});
