/**
 * aiSecurityScanSafe() / gate.ts's runSecretAndSecurityScan — availability
 * vs verdict, mirroring gate-gatetest-availability.test.ts's pattern for
 * the exact same class of bug in a different gate.
 *
 * Before this fix, aiSecurityScan() returned `[]` on an AI-provider error
 * (timeout, rate limit, unparseable response) — indistinguishable from a
 * genuinely clean scan. gate.ts then reported "No security issues found",
 * silently converting a merge-blocking security gate into an automatic
 * pass on any Anthropic outage. Fixed the same way runGateTestScan's
 * outage bug was fixed (commit 6930df0): don't hard-block on a third-party
 * outage, but never claim "clean" when the scan didn't actually run.
 */

import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { runSecretAndSecurityScan } from "../lib/gate";
import { config } from "../lib/config";
import { __resetAnthropicClientForTests } from "../lib/ai-client";

const origFetch = globalThis.fetch;
const origKey = process.env.ANTHROPIC_API_KEY;

function mockFetch(responder: () => Response | Promise<Response>): void {
  // @ts-expect-error — override global fetch for the test
  globalThis.fetch = async (): Promise<Response> => responder();
}

function anthropicResponse(text: string): Response {
  return new Response(
    JSON.stringify({
      id: "msg_test",
      type: "message",
      role: "assistant",
      model: "claude-sonnet-4-6",
      content: [{ type: "text", text }],
      stop_reason: "end_turn",
      stop_sequence: null,
      usage: { input_tokens: 10, output_tokens: 10 },
    }),
    { status: 200, headers: { "content-type": "application/json" } }
  );
}

beforeEach(() => {
  process.env.ANTHROPIC_API_KEY = "sk-ant-test-key";
  // The Anthropic SDK client is a module-level singleton (ai-client.ts's
  // `_client`) that captures a fetch reference at construction time rather
  // than reading globalThis.fetch fresh per call — without resetting it,
  // only the FIRST test's globalThis.fetch mock would ever actually be
  // used, and every later test would silently reuse it (confirmed: this
  // leaked a 429-mock response into an unrelated later test before this
  // reset was added).
  __resetAnthropicClientForTests();
});

afterEach(() => {
  globalThis.fetch = origFetch;
  if (origKey === undefined) delete process.env.ANTHROPIC_API_KEY;
  else process.env.ANTHROPIC_API_KEY = origKey;
  __resetAnthropicClientForTests();
});

describe("runSecretAndSecurityScan — security scan availability vs verdict", () => {
  it("is skipped (not blocking) when the AI provider errors — was previously silently 'clean'", async () => {
    mockFetch(() => new Response("rate limited", { status: 429 }));
    const result = await runSecretAndSecurityScan("o", "r", "refs/heads/main", "a".repeat(40), {
      scanSecrets: false,
      scanSecurity: true,
      diffText: "diff --git a/x.ts b/x.ts\n+ eval(userInput)",
    });
    expect(result.securityResult.skipped).toBe(true);
    expect(result.securityResult.passed).toBe(true);
    expect(result.securityResult.details).not.toBe("No security issues found");
    expect(result.securityResult.details).toContain("unavailable");
  });

  it("is skipped (not blocking) when the request throws — network error / timeout", async () => {
    mockFetch(() => {
      throw new Error("ETIMEDOUT");
    });
    const result = await runSecretAndSecurityScan("o", "r", "refs/heads/main", "a".repeat(40), {
      scanSecrets: false,
      scanSecurity: true,
      diffText: "some diff",
    });
    expect(result.securityResult.skipped).toBe(true);
    expect(result.securityResult.passed).toBe(true);
    // The Anthropic SDK wraps a thrown-during-fetch error into its own
    // connection/timeout error type rather than preserving the original
    // message verbatim — assert on the outcome (skipped, not the clean
    // string), not the SDK's exact wording.
    expect(result.securityResult.details).not.toBe("No security issues found");
    expect(result.securityResult.details).toContain("unavailable");
  });

  it("is skipped (not blocking) when the AI response is unparseable", async () => {
    mockFetch(() => anthropicResponse("not json at all, just prose"));
    const result = await runSecretAndSecurityScan("o", "r", "refs/heads/main", "a".repeat(40), {
      scanSecrets: false,
      scanSecurity: true,
      diffText: "some diff",
    });
    expect(result.securityResult.skipped).toBe(true);
    expect(result.securityResult.passed).toBe(true);
    expect(result.securityResult.details).not.toBe("No security issues found");
  });

  // Regression guard: proves the two states are now actually distinguishable
  // — a genuine clean scan must still say "No security issues found" and
  // must NOT be marked skipped.
  it("is NOT skipped and reports a genuine clean verdict on success with zero findings", async () => {
    mockFetch(() => anthropicResponse('```json\n{"findings": []}\n```'));
    const result = await runSecretAndSecurityScan("o", "r", "refs/heads/main", "a".repeat(40), {
      scanSecrets: false,
      scanSecurity: true,
      diffText: "some diff",
    });
    expect(result.securityResult.skipped).toBe(false);
    expect(result.securityResult.passed).toBe(true);
    expect(result.securityResult.details).toBe("No security issues found");
  });

  it("blocks on a genuine finding — real verdict, not an outage", async () => {
    mockFetch(() =>
      anthropicResponse(
        '```json\n{"findings": [{"type": "Command Injection", "file": "x.ts", "line": 1, "severity": "critical", "description": "eval on user input"}]}\n```'
      )
    );
    const result = await runSecretAndSecurityScan("o", "r", "refs/heads/main", "a".repeat(40), {
      scanSecrets: false,
      scanSecurity: true,
      diffText: "some diff",
    });
    expect(result.securityResult.skipped).toBe(false);
    expect(result.securityResult.passed).toBe(false);
    expect(result.securityIssues).toHaveLength(1);
  });

  it("is skipped with 'no diff provided' when scanSecurity is requested but no diffText is given", async () => {
    const result = await runSecretAndSecurityScan("o", "r", "refs/heads/main", "a".repeat(40), {
      scanSecrets: false,
      scanSecurity: true,
    });
    expect(result.securityResult.skipped).toBe(true);
    expect(result.securityResult.passed).toBe(true);
    expect(result.securityResult.details).toBe("Skipped — no diff provided");
  });
});

// Keep config import referenced — sanity that config.anthropicApiKey reflects env for this test file.
describe("config.anthropicApiKey reflects ANTHROPIC_API_KEY for this suite", () => {
  it("resolves to the mocked value while set", () => {
    expect(config.anthropicApiKey).toBe("sk-ant-test-key");
  });
});
