import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "node:fs";
import path from "node:path";
import { type Cleanup, sandboxHome } from "./_helpers/sandbox";

// ── detect.ts tests ─────────────────────────────────────────────────────────

describe("detectAgentPlatforms", () => {
  let testHome: string;
  let envCleanup: Cleanup = () => {};

  beforeEach(() => {
    const homeResult = sandboxHome();
    testHome = homeResult.dir;
    envCleanup = homeResult.cleanup;
  });

  afterEach(() => {
    envCleanup();
    envCleanup = () => {};
  });

  test("returns empty array when no platforms found", async () => {
    const { detectAgentPlatforms } = await import("../src/setup/detect");
    const result = detectAgentPlatforms();
    expect(result).toEqual([]);
  });

  test("detects .claude directory", async () => {
    fs.mkdirSync(path.join(testHome, ".claude"), { recursive: true });
    // Re-import to pick up fresh HOME
    const { detectAgentPlatforms } = await import("../src/setup/detect");
    const result = detectAgentPlatforms();
    const claude = result.find((p) => p.name === "Claude Code");
    expect(claude).toBeDefined();
    expect(claude?.path).toBe(path.join(testHome, ".claude"));
  });

  test("detects .config/opencode directory", async () => {
    fs.mkdirSync(path.join(testHome, ".config", "opencode"), { recursive: true });
    const { detectAgentPlatforms } = await import("../src/setup/detect");
    const result = detectAgentPlatforms();
    const opencode = result.find((p) => p.name === "OpenCode");
    expect(opencode).toBeDefined();
    expect(opencode?.path).toBe(path.join(testHome, ".config", "opencode"));
  });

  test("detects multiple session-log-capable platforms", async () => {
    fs.mkdirSync(path.join(testHome, ".claude"), { recursive: true });
    fs.mkdirSync(path.join(testHome, ".config", "opencode"), { recursive: true });
    const { detectAgentPlatforms } = await import("../src/setup/detect");
    const result = detectAgentPlatforms();
    const names = result.map((p) => p.name).sort();
    expect(names).toEqual(["Claude Code", "OpenCode"]);
  });

  // #567 — detection-trap fix. The old AGENT_PLATFORMS list offered four
  // harnesses with no session-log provider (Continue, Codeium/Windsurf, Cursor,
  // Codex CLI). Selecting them added a stash source that was never indexed — a
  // silent no-op. detectAgentPlatforms now derives only from
  // SESSION_LOG_HARNESSES, so those config dirs are NOT offered even when present.
  test("does NOT offer harnesses with no session-log provider (Continue/Codeium/Cursor/Codex)", async () => {
    fs.mkdirSync(path.join(testHome, ".continue"), { recursive: true });
    fs.mkdirSync(path.join(testHome, ".codeium"), { recursive: true });
    fs.mkdirSync(path.join(testHome, ".cursor"), { recursive: true });
    fs.mkdirSync(path.join(testHome, ".codex"), { recursive: true });
    const { detectAgentPlatforms } = await import("../src/setup/detect");
    const result = detectAgentPlatforms();
    // None of the dead-option dirs surface as candidates.
    expect(result).toEqual([]);
  });

  // The candidate list must equal the session-log-capable harnesses that
  // declare a setup detection dir — the registry is the single source.
  test("offered platforms equal SESSION_LOG_HARNESSES with a setupDetectionDir", async () => {
    fs.mkdirSync(path.join(testHome, ".claude"), { recursive: true });
    fs.mkdirSync(path.join(testHome, ".config", "opencode"), { recursive: true });
    fs.mkdirSync(path.join(testHome, ".cursor"), { recursive: true });
    const { detectAgentPlatforms } = await import("../src/setup/detect");
    const { SESSION_LOG_HARNESSES } = await import("../src/integrations/harnesses");
    const expected = SESSION_LOG_HARNESSES.filter((h) => h.setupDetectionDir)
      .map((h) => h.displayName)
      .sort();
    const got = detectAgentPlatforms()
      .map((p) => p.name)
      .sort();
    expect(got).toEqual(expected);
  });

  test("ignores files (only detects directories)", async () => {
    fs.writeFileSync(path.join(testHome, ".claude"), "not a directory");
    const { detectAgentPlatforms } = await import("../src/setup/detect");
    const result = detectAgentPlatforms();
    const claude = result.find((p) => p.name === "Claude Code");
    expect(claude).toBeUndefined();
  });

  test("returns empty when HOME and USERPROFILE are both unset", async () => {
    delete process.env.HOME;
    delete process.env.USERPROFILE;
    const { detectAgentPlatforms } = await import("../src/setup/detect");
    const result = detectAgentPlatforms();
    expect(result).toEqual([]);
  });

  test("falls back to USERPROFILE when HOME is unset (Windows)", async () => {
    delete process.env.HOME;
    process.env.USERPROFILE = testHome;
    fs.mkdirSync(path.join(testHome, ".claude"), { recursive: true });
    const { detectAgentPlatforms } = await import("../src/setup/detect");
    const result = detectAgentPlatforms();
    const claude = result.find((p) => p.name === "Claude Code");
    expect(claude).toBeDefined();
    expect(claude?.path).toBe(path.join(testHome, ".claude"));
    delete process.env.USERPROFILE;
  });
});

describe("detectOllama", () => {
  let originalFetch: typeof globalThis.fetch;

  beforeEach(() => {
    originalFetch = globalThis.fetch;
  });

  afterEach(() => {
    globalThis.fetch = originalFetch;
  });

  test("returns available=true with models from API", async () => {
    globalThis.fetch = (async (input: string | URL | Request) => {
      const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
      if (url.includes("/api/tags")) {
        return new Response(
          JSON.stringify({
            models: [{ name: "llama3.2:latest" }, { name: "nomic-embed-text:latest" }, { name: "codellama:latest" }],
          }),
          { status: 200 },
        );
      }
      return new Response("", { status: 404 });
    }) as unknown as typeof fetch;

    const { detectOllama } = await import("../src/setup/detect");
    const result = await detectOllama();
    expect(result.available).toBe(true);
    expect(result.models).toContain("llama3.2");
    expect(result.models).toContain("nomic-embed-text");
    expect(result.models).toContain("codellama");
  });

  test("strips :latest suffix from model names", async () => {
    globalThis.fetch = (async () => {
      return new Response(
        JSON.stringify({
          models: [{ name: "llama3.2:latest" }, { name: "phi3:v2" }],
        }),
        { status: 200 },
      );
    }) as unknown as typeof fetch;

    const { detectOllama } = await import("../src/setup/detect");
    const result = await detectOllama();
    expect(result.models).toContain("llama3.2");
    expect(result.models).toContain("phi3:v2");
    expect(result.models).not.toContain("llama3.2:latest");
  });

  test("returns available=false when fetch fails", async () => {
    globalThis.fetch = (async () => {
      throw new Error("Connection refused");
    }) as unknown as typeof fetch;

    const { detectOllama } = await import("../src/setup/detect");
    const result = await detectOllama();
    // May still be available=true if `ollama list` CLI works, or false if both fail
    // Just verify it doesn't throw
    expect(typeof result.available).toBe("boolean");
    expect(Array.isArray(result.models)).toBe(true);
  });

  test("returns sorted model names", async () => {
    globalThis.fetch = (async () => {
      return new Response(
        JSON.stringify({
          models: [{ name: "zephyr:latest" }, { name: "alpaca:latest" }, { name: "mistral:latest" }],
        }),
        { status: 200 },
      );
    }) as unknown as typeof fetch;

    const { detectOllama } = await import("../src/setup/detect");
    const result = await detectOllama();
    expect(result.models).toEqual(["alpaca", "mistral", "zephyr"]);
  });
});
