import { describe, it, expect } from "vitest";
import path from "node:path";
import os from "node:os";
import fs from "node:fs";
import {
  findNextTask,
  findFirstUncheckedBullet,
  buildTaskSpec,
  resolveClaudeAuth,
} from "./dispatch.mjs";

function makeWorkTree({ epics }) {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "dispatch-"));
  for (const [epicId, epicData] of Object.entries(epics)) {
    const epicDir = path.join(root, epicId);
    fs.mkdirSync(epicDir, { recursive: true });
    fs.writeFileSync(
      path.join(epicDir, "_epic.md"),
      `---\nid: ${epicId}\ntitle: ${epicId}\nstatus: ${epicData.status ?? "in-progress"}\n---\n`,
    );
    for (const [storyId, storyData] of Object.entries(epicData.stories ?? {})) {
      const storyDir = path.join(epicDir, storyId);
      fs.mkdirSync(storyDir, { recursive: true });
      const tasks = (storyData.tasks ?? [])
        .map((t) => `- [${t.done ? "x" : " "}] ${t.label}`)
        .join("\n");
      const deps = storyData.depends_on
        ? `depends-on: [${storyData.depends_on.join(", ")}]\n`
        : "";
      fs.writeFileSync(
        path.join(storyDir, "_story.md"),
        `---\nid: ${storyId}\ntitle: ${storyId}\nstatus: ${storyData.status ?? "todo"}\n${deps}---\n\n## Tasks\n${tasks}\n`,
      );
    }
  }
  return root;
}

describe("findFirstUncheckedBullet", () => {
  it("returns the first - [ ] line under ## Tasks", () => {
    const content = `## Tasks
- [x] done
- [ ] open
- [ ] another
`;
    const { bulletLine } = findFirstUncheckedBullet(content);
    expect(bulletLine).toBe("- [ ] open");
  });

  it("returns null when no unchecked bullets remain", () => {
    const content = `## Tasks
- [x] done
- [x] also done
`;
    expect(findFirstUncheckedBullet(content).bulletLine).toBeNull();
  });

  it("only checks inside the Tasks section", () => {
    const content = `## Goal
- [ ] not a task

## Tasks
- [x] done
`;
    expect(findFirstUncheckedBullet(content).bulletLine).toBeNull();
  });
});

describe("findNextTask", () => {
  it("returns the next bullet from the first ready story", () => {
    const root = makeWorkTree({
      epics: {
        e: {
          stories: {
            s: {
              status: "todo",
              tasks: [
                { label: "a", done: true },
                { label: "b", done: false },
              ],
            },
          },
        },
      },
    });
    const next = findNextTask(root);
    expect(next.epic).toBe("e");
    expect(next.story).toBe("s");
    expect(next.bulletLine.trim()).toBe("- [ ] b");
  });

  it("returns null when no stories are ready", () => {
    const root = makeWorkTree({
      epics: {
        e: {
          stories: {
            s: {
              status: "done",
              tasks: [{ label: "a", done: true }],
            },
          },
        },
      },
    });
    expect(findNextTask(root)).toBeNull();
  });

  it("returns null when ready story has no unchecked bullets", () => {
    const root = makeWorkTree({
      epics: {
        e: {
          stories: {
            s: {
              status: "todo",
              tasks: [{ label: "a", done: true }],
            },
          },
        },
      },
    });
    expect(findNextTask(root)).toBeNull();
  });
});

describe("buildTaskSpec", () => {
  it("includes epic, story, bullet, and full story content", () => {
    const next = {
      epic: "e",
      story: "s",
      title: "Story",
      storyContent: "## Goal\n\nSomething.\n## Tasks\n- [ ] do thing",
      bulletLine: "- [ ] do thing",
    };
    const spec = buildTaskSpec(next);
    expect(spec).toContain("e");
    expect(spec).toContain("s — Story");
    expect(spec).toContain("- [ ] do thing");
    expect(spec).toContain("## Goal");
  });
});

describe("resolveClaudeAuth", () => {
  it("returns subscription mode when ~/.claude exists on host", () => {
    const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "auth-sub-"));
    fs.mkdirSync(path.join(tmpHome, ".claude"));
    const result = resolveClaudeAuth({ env: {}, home: tmpHome });
    expect(result.mode).toBe("subscription");
    expect(result.hostPath).toBe(path.join(tmpHome, ".claude"));
    expect(result.sandboxPath).toBe("~/.claude");
  });

  it("honours SANDCASTLE_CLAUDE_CREDS_DIR override", () => {
    const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "auth-override-"));
    const overrideDir = path.join(tmpRoot, "custom-claude");
    fs.mkdirSync(overrideDir);
    const result = resolveClaudeAuth({
      env: { SANDCASTLE_CLAUDE_CREDS_DIR: overrideDir },
      home: "/nonexistent",
    });
    expect(result.mode).toBe("subscription");
    expect(result.hostPath).toBe(overrideDir);
  });

  it("falls back to ANTHROPIC_API_KEY when ~/.claude does not exist", () => {
    const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "auth-key-"));
    // No .claude directory created
    const result = resolveClaudeAuth({
      env: { ANTHROPIC_API_KEY: "sk-test" },
      home: tmpHome,
    });
    expect(result.mode).toBe("api-key");
    expect(result.env).toEqual({ ANTHROPIC_API_KEY: "sk-test" });
  });

  it("falls back to OPENAI_API_KEY when only that is set", () => {
    const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "auth-openai-"));
    const result = resolveClaudeAuth({
      env: { OPENAI_API_KEY: "sk-openai" },
      home: tmpHome,
    });
    expect(result.mode).toBe("api-key");
    expect(result.env).toEqual({ OPENAI_API_KEY: "sk-openai" });
  });

  it("returns missing when neither subscription nor API key available", () => {
    const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "auth-missing-"));
    const result = resolveClaudeAuth({ env: {}, home: tmpHome });
    expect(result.mode).toBe("missing");
  });

  it("prefers subscription over API key when both available", () => {
    const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "auth-both-"));
    fs.mkdirSync(path.join(tmpHome, ".claude"));
    const result = resolveClaudeAuth({
      env: { ANTHROPIC_API_KEY: "sk-test" },
      home: tmpHome,
    });
    expect(result.mode).toBe("subscription");
  });
});
