/**
 * Tests for src/lib/ai-review.ts.
 *
 * The Anthropic-calling path (reviewDiff) and the DB-touching paths
 * (triggerAiReview's idempotency probe + insert) require external
 * services we don't stand up in unit tests. We therefore cover the
 * pure invariants:
 *
 *   - The marker constant is stable so future searches keep working.
 *   - isAiReviewEnabled is a boolean reflecting config.anthropicApiKey.
 *   - triggerAiReview is a no-op (resolves cleanly) when the API key
 *     is absent — this is the documented graceful-degrade contract.
 *   - triggerAiReview never throws even when called with garbage.
 *   - The internal __test helpers are exported and shaped correctly.
 */

import { describe, it, expect } from "bun:test";
import {
  AI_REVIEW_MARKER,
  isAiReviewEnabled,
  triggerAiReview,
  computeAiReviewApproval,
  __test,
} from "../lib/ai-review";

describe("AI_REVIEW_MARKER", () => {
  it("is the documented stable string", () => {
    // Important: any change to this string is a breaking change for
    // idempotency. New version → write a migration to back-fill old
    // markers so older AI summaries still suppress duplicates.
    expect(AI_REVIEW_MARKER).toBe("<!-- gluecron-ai-review:summary -->");
  });

  it("is an HTML comment so it doesn't render in markdown", () => {
    expect(AI_REVIEW_MARKER.startsWith("<!--")).toBe(true);
    expect(AI_REVIEW_MARKER.endsWith("-->")).toBe(true);
  });
});

describe("isAiReviewEnabled", () => {
  it("returns a boolean", () => {
    const v = isAiReviewEnabled();
    expect(typeof v).toBe("boolean");
  });
});

describe("triggerAiReview — graceful degrade + crash-free", () => {
  // Note: in the test sandbox ANTHROPIC_API_KEY is unset, so the
  // function should resolve immediately without touching the DB. We
  // also pass garbage repo names + nonexistent PR ids to confirm the
  // overall try/catch holds.
  it("resolves without throwing when API key is absent", async () => {
    const before = process.env.ANTHROPIC_API_KEY;
    delete process.env.ANTHROPIC_API_KEY;
    try {
      await triggerAiReview(
        "alice",
        "demo",
        "00000000-0000-0000-0000-000000000000",
        "Test PR",
        "Body",
        "main",
        "feature"
      );
    } finally {
      if (before) process.env.ANTHROPIC_API_KEY = before;
    }
    expect(true).toBe(true);
  });

  it("never throws even with invalid inputs", async () => {
    let threw = false;
    try {
      await triggerAiReview(
        "",
        "",
        "not-a-uuid",
        "",
        "",
        "",
        ""
      );
    } catch {
      threw = true;
    }
    expect(threw).toBe(false);
  });

  it("survives an unknown branch combination without throwing", async () => {
    let threw = false;
    try {
      await triggerAiReview(
        "definitely-not-a-real-owner",
        "definitely-not-a-real-repo",
        "00000000-0000-0000-0000-000000000000",
        "Title",
        "Body",
        "definitely-not-a-real-base",
        "definitely-not-a-real-head"
      );
    } catch {
      threw = true;
    }
    expect(threw).toBe(false);
  });
});

describe("computeAiReviewApproval — merge-gate decision", () => {
  // Regression coverage for a real bug: three call sites (the MCP merge
  // tool, the PR-page gate preview, and the web merge handler) each had
  // their own ad-hoc heuristic for "did AI review approve this", and none
  // of them matched the literal verdict strings triggerAiReview actually
  // writes -- so the MCP path in particular hard-blocked every merge the
  // instant AI review ran at all, clean or not. Reproduced live via
  // scripts/agent-journey.ts against production.
  const marker = AI_REVIEW_MARKER;

  it("approves when there are no AI comments at all", () => {
    expect(computeAiReviewApproval([])).toBe(true);
  });

  it("approves on the real clean-verdict string", () => {
    const body = `${marker}\n## AI Code Review\n\n**AI review:** no blocking issues found.\n\nLooks good.`;
    expect(computeAiReviewApproval([{ body, createdAt: new Date() }])).toBe(true);
  });

  it("blocks on the real flagged-items verdict string", () => {
    const body = `${marker}\n## AI Code Review\n\n**AI review:** flagged 2 item(s) for human attention.\n\nSee inline comments.`;
    expect(computeAiReviewApproval([{ body, createdAt: new Date() }])).toBe(false);
  });

  it("fails open on 'AI review unavailable' (provider error, e.g. no API credit)", () => {
    const body = `${marker}\n## AI review unavailable\n\nThe AI review attempt failed: 400 credit balance too low. The PR is otherwise unchanged.`;
    expect(computeAiReviewApproval([{ body, createdAt: new Date() }])).toBe(true);
  });

  it("fails open on 'AI review skipped' (quota exhausted)", () => {
    const body = `${marker}\n## AI review skipped\n\nYour monthly AI token budget has been reached.`;
    expect(computeAiReviewApproval([{ body, createdAt: new Date() }])).toBe(true);
  });

  it("ignores non-summary AI comments (e.g. inline findings, triage) with no marker", () => {
    const inlineFinding = { body: "This line looks risky.", createdAt: new Date() };
    expect(computeAiReviewApproval([inlineFinding])).toBe(true);
  });

  it("uses the most recent summary when a PR was re-reviewed after a push", () => {
    const older = {
      body: `${marker}\n**AI review:** flagged 1 item(s) for human attention.`,
      createdAt: new Date("2026-01-01T00:00:00Z"),
    };
    const newer = {
      body: `${marker}\n**AI review:** no blocking issues found.`,
      createdAt: new Date("2026-01-02T00:00:00Z"),
    };
    expect(computeAiReviewApproval([older, newer])).toBe(true);
    expect(computeAiReviewApproval([newer, older])).toBe(true);
  });
});

describe("__test internals", () => {
  it("exports diffBetweenBranches and alreadyReviewed", () => {
    expect(typeof __test.diffBetweenBranches).toBe("function");
    expect(typeof __test.alreadyReviewed).toBe("function");
  });

  it("diffBetweenBranches returns '' for a nonexistent repo", async () => {
    const out = await __test.diffBetweenBranches(
      "definitely-not-a-real-owner",
      "definitely-not-a-real-repo",
      "main",
      "feature"
    );
    expect(out).toBe("");
  });

  it("alreadyReviewed returns false for an unknown PR id (fail-open)", async () => {
    const out = await __test.alreadyReviewed(
      "00000000-0000-0000-0000-000000000000",
      "0000000000000000000000000000000000000000"
    );
    expect(out).toBe(false);
  });
});
