// Tests for the extract prompt builder + schema + parser.

import { describe, expect, test } from "bun:test";
import {
  buildExtractPrompt,
  EXTRACT_JSON_SCHEMA,
  type ExtractPayload,
  parseExtractPayload,
} from "../src/commands/improve/extract-prompt";
import type { SessionData } from "../src/integrations/session-logs/types";

function sampleData(overrides: Partial<SessionData> = {}): SessionData {
  return {
    ref: {
      harness: "claude-code",
      sessionId: "ses_test",
      filePath: "/tmp/test.jsonl",
      startedAt: Date.parse("2026-05-26T10:00:00.000Z"),
      endedAt: Date.parse("2026-05-26T11:00:00.000Z"),
      projectHint: "test-project",
      title: "Test session",
    },
    events: [],
    inlineRefs: [],
    ...overrides,
  };
}

// ── Schema shape ────────────────────────────────────────────────────────────

describe("EXTRACT_JSON_SCHEMA", () => {
  test("round-trips through JSON.parse(JSON.stringify(...))", () => {
    const cloned = JSON.parse(JSON.stringify(EXTRACT_JSON_SCHEMA));
    expect(cloned).toEqual(EXTRACT_JSON_SCHEMA);
  });

  test("requires candidates at the top level", () => {
    const s = EXTRACT_JSON_SCHEMA as { required: string[] };
    expect(s.required).toContain("candidates");
  });

  test("forbids additionalProperties at top level", () => {
    const s = EXTRACT_JSON_SCHEMA as { additionalProperties: boolean };
    expect(s.additionalProperties).toBe(false);
  });

  test("candidate items require type, name, description, body, confidence, evidence", () => {
    const candItem = (EXTRACT_JSON_SCHEMA as { properties: Record<string, { items: { required: string[] } }> })
      .properties.candidates.items;
    for (const key of ["type", "name", "description", "body", "confidence", "evidence"]) {
      expect(candItem.required).toContain(key);
    }
  });

  test("type enum restricts to memory|lesson|knowledge", () => {
    const typeProp = (
      EXTRACT_JSON_SCHEMA as {
        properties: { candidates: { items: { properties: { type: { enum: string[] } } } } };
      }
    ).properties.candidates.items.properties.type;
    expect(typeProp.enum).toEqual(["memory", "lesson", "knowledge"]);
  });

  test("confidence bounded to [0, 1]", () => {
    const confProp = (
      EXTRACT_JSON_SCHEMA as {
        properties: { candidates: { items: { properties: { confidence: { minimum: number; maximum: number } } } } };
      }
    ).properties.candidates.items.properties.confidence;
    expect(confProp.minimum).toBe(0);
    expect(confProp.maximum).toBe(1);
  });
});

// ── Prompt builder ──────────────────────────────────────────────────────────

describe("buildExtractPrompt", () => {
  test("interpolates harness, title, dates, project hint into the template", () => {
    const prompt = buildExtractPrompt({ data: sampleData(), events: [], inlineRefs: [] });
    expect(prompt).toContain("claude-code");
    expect(prompt).toContain("Test session");
    expect(prompt).toContain("test-project");
    expect(prompt).toContain("2026-05-26T10:00:00.000Z");
    expect(prompt).toContain("2026-05-26T11:00:00.000Z");
  });

  test("renders empty inlineRefs as a sentinel string", () => {
    const prompt = buildExtractPrompt({ data: sampleData(), events: [], inlineRefs: [] });
    expect(prompt).toContain("(none —");
  });

  test("renders inlineRefs as a bullet list", () => {
    const prompt = buildExtractPrompt({
      data: sampleData(),
      events: [],
      inlineRefs: [
        { kind: "remember", text: "VPN required before deploy" },
        { kind: "feedback", ref: "knowledge:auth", text: "saved time on debug" },
      ],
    });
    expect(prompt).toContain("- remember: VPN required before deploy");
    expect(prompt).toContain("- feedback knowledge:auth: saved time on debug");
  });

  test("truncates long inline-ref text to keep the prompt focused", () => {
    const longText = "a".repeat(500);
    const prompt = buildExtractPrompt({
      data: sampleData(),
      events: [],
      inlineRefs: [{ kind: "remember", text: longText }],
    });
    expect(prompt).toMatch(/a{200}…/);
  });

  test("renders empty events as a sentinel", () => {
    const prompt = buildExtractPrompt({ data: sampleData(), events: [], inlineRefs: [] });
    expect(prompt).toContain("(empty");
  });

  test("renders events with role @ timestamp @ text format", () => {
    const ts = Date.parse("2026-05-26T10:30:00.000Z");
    const prompt = buildExtractPrompt({
      data: sampleData(),
      events: [
        {
          harness: "claude-code",
          text: "agent recovered after Bash failure",
          ts,
          role: "assistant",
          filePath: "/tmp/x",
        },
      ],
      inlineRefs: [],
    });
    expect(prompt).toContain("[assistant @ 2026-05-26T10:30:00.000Z] agent recovered after Bash failure");
  });

  test("handles missing optional metadata gracefully", () => {
    const data: SessionData = {
      ref: { harness: "opencode", sessionId: "x", filePath: "/tmp/y" },
      events: [],
      inlineRefs: [],
    };
    const prompt = buildExtractPrompt({ data, events: [], inlineRefs: [] });
    expect(prompt).toContain("(no title)");
    expect(prompt).toContain("(no project hint)");
    expect(prompt).toContain("unknown"); // for startedAt/endedAt
  });
});

// ── Parser ──────────────────────────────────────────────────────────────────

describe("parseExtractPayload", () => {
  test("returns empty candidates when stdout is empty", () => {
    expect(parseExtractPayload("")).toMatchObject({ candidates: [] });
  });

  test("parses a valid memory candidate", () => {
    const payload: ExtractPayload = {
      candidates: [
        {
          type: "memory",
          name: "auth-uses-jwt",
          description: "Auth pipeline uses JWT tokens with 24h TTL instead of session cookies.",
          body: "The auth module switched from session-cookie storage to short-lived JWT tokens in May. TTL is 24h.\n",
          confidence: 0.85,
          evidence: "user's correction at 2026-05-26T10:15Z",
        },
      ],
    };
    const out = parseExtractPayload(JSON.stringify(payload));
    expect(out.candidates).toHaveLength(1);
    expect(out.candidates[0]).toMatchObject({ type: "memory", name: "auth-uses-jwt", confidence: 0.85 });
  });

  test("parses a lesson candidate with when_to_use", () => {
    const payload: ExtractPayload = {
      candidates: [
        {
          type: "lesson",
          name: "vpn-before-deploy",
          description: "Always connect to the corporate VPN before running deploy.sh — otherwise stage rollouts hang.",
          when_to_use: "When initiating a production deploy from a fresh shell or laptop reboot.",
          body: "Repeatedly observed: deploy.sh hangs at the 'pushing to stage' step when VPN is not connected. The script reports a misleading network error.",
          confidence: 0.92,
          evidence: "tool failure in Bash at 2026-05-26T10:30Z + agent recovery",
        },
      ],
    };
    const out = parseExtractPayload(JSON.stringify(payload));
    expect(out.candidates).toHaveLength(1);
    expect(out.candidates[0]?.when_to_use).toBe(
      "When initiating a production deploy from a fresh shell or laptop reboot.",
    );
  });

  test("rejects lesson candidates missing when_to_use", () => {
    const payload = {
      candidates: [
        {
          type: "lesson",
          name: "missing-wtu",
          description: "This is a valid-length description that does not specify when to use the lesson.",
          body: "Some body content that is fifty characters or more in length to pass the minimum body check.",
          confidence: 0.8,
          evidence: "fake evidence pointer",
        },
      ],
    };
    const out = parseExtractPayload(JSON.stringify(payload));
    expect(out.candidates).toHaveLength(0);
  });

  test("rejects candidates with placeholder/invalid descriptions", () => {
    const payload = {
      candidates: [
        {
          type: "memory",
          name: "short-desc",
          description: "too short",
          body: "x".repeat(60),
          confidence: 0.8,
          evidence: "ev",
        },
        { type: "memory", name: "no-desc", description: "", body: "x".repeat(60), confidence: 0.8, evidence: "ev" },
      ],
    };
    const out = parseExtractPayload(JSON.stringify(payload));
    expect(out.candidates).toHaveLength(0);
  });

  test("clamps out-of-range confidence to [0, 1]", () => {
    const payload = {
      candidates: [
        {
          type: "memory",
          name: "over",
          description: "Description that's long enough to pass the twenty-char minimum check easily.",
          body: "Body content that is at least fifty characters long for the parser to keep it.",
          confidence: 1.5,
          evidence: "evidence here",
        },
      ],
    };
    const out = parseExtractPayload(JSON.stringify(payload));
    expect(out.candidates[0]?.confidence).toBe(1);
  });

  test("tolerates prose preamble around the JSON object", () => {
    const noisy = `Here's my analysis:\n\n{"candidates": []}\n\nLet me know if you want more.`;
    const out = parseExtractPayload(noisy);
    expect(out).toEqual({ candidates: [] });
  });

  test("preserves rationale_if_empty when present", () => {
    const out = parseExtractPayload(
      JSON.stringify({ candidates: [], rationale_if_empty: "Session contained only akm meta-ops, nothing durable." }),
    );
    expect(out.rationale_if_empty).toBe("Session contained only akm meta-ops, nothing durable.");
  });

  test("drops candidates with invalid type values", () => {
    const payload = {
      candidates: [
        {
          type: "skill", // not in the enum
          name: "bad-type",
          description: "Description that's long enough to pass the twenty-char minimum check.",
          body: "Body content that is at least fifty characters long for the parser to keep it intact.",
          confidence: 0.8,
          evidence: "evidence here",
        },
      ],
    };
    const out = parseExtractPayload(JSON.stringify(payload));
    expect(out.candidates).toHaveLength(0);
  });

  test("rejects candidates with malformed names", () => {
    const payload = {
      candidates: [
        {
          type: "memory",
          name: "Bad Name With Spaces",
          description: "Description that's long enough to pass the twenty-char minimum check.",
          body: "Body content that is at least fifty characters long for the parser to keep it intact.",
          confidence: 0.8,
          evidence: "evidence here",
        },
      ],
    };
    const out = parseExtractPayload(JSON.stringify(payload));
    expect(out.candidates).toHaveLength(0);
  });

  test("returns empty payload for completely non-JSON input", () => {
    const out = parseExtractPayload("This is just prose with no JSON object at all.");
    expect(out.candidates).toEqual([]);
    expect(out.rationale_if_empty).toMatch(/not parseable/);
  });
});
