import { describe, it, expect } from "vitest";
import path from "node:path";
import os from "node:os";
import fs from "node:fs";
import {
  buildState,
  parseFrontmatter,
  countTaskCheckboxes,
  computeReadyBlocked,
} from "./state-builder.mjs";

function makeWorkTree({ epics }) {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-state-"));
  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"),
      `---
id: ${epicId}
title: ${epicData.title ?? epicId}
status: ${epicData.status ?? "todo"}
---

`,
    );
    for (const [storyId, storyData] of Object.entries(epicData.stories ?? {})) {
      const storyDir = path.join(epicDir, storyId);
      fs.mkdirSync(storyDir, { recursive: true });
      const tasksBlock = (storyData.tasks ?? [])
        .map((t) => `- [${t.done ? "x" : " "}] ${t.label}`)
        .join("\n");
      fs.writeFileSync(
        path.join(storyDir, "_story.md"),
        `---
id: ${storyId}
title: ${storyData.title ?? storyId}
status: ${storyData.status ?? "todo"}
---

## Tasks
${tasksBlock}
`,
      );
    }
  }
  return root;
}

describe("buildState", () => {
  it("returns an empty epics map for a non-existent workRoot", () => {
    const state = buildState("/nonexistent/path");
    expect(state.epics).toEqual({});
    expect(typeof state.updated_at).toBe("string");
  });

  it("collects epics + stories with status + task counts", () => {
    const root = makeWorkTree({
      epics: {
        epic1: {
          status: "in-progress",
          stories: {
            story1: {
              status: "done",
              tasks: [
                { label: "a", done: true },
                { label: "b", done: true },
              ],
            },
            story2: {
              status: "todo",
              tasks: [
                { label: "c", done: false },
                { label: "d", done: false },
                { label: "e", done: false },
              ],
            },
          },
        },
      },
    });

    const state = buildState(root);
    expect(state.epics).toEqual({
      epic1: {
        status: "in-progress",
        title: "epic1",
        stories: {
          story1: {
            status: "done",
            title: "story1",
            ac_total: 2,
            ac_completed: 2,
            depends_on: [],
            blocks: [],
          },
          story2: {
            status: "todo",
            title: "story2",
            ac_total: 3,
            ac_completed: 0,
            depends_on: [],
            blocks: [],
          },
        },
      },
    });
  });

  it("skips _templates and prds folders", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-skip-"));
    fs.mkdirSync(path.join(root, "_templates"), { recursive: true });
    fs.writeFileSync(
      path.join(root, "_templates", "_epic.md"),
      "---\nid: x\n---",
    );
    fs.mkdirSync(path.join(root, "prds"), { recursive: true });
    fs.writeFileSync(path.join(root, "prds", "_epic.md"), "---\nid: y\n---");
    expect(buildState(root).epics).toEqual({});
  });

  it("skips directories without _epic.md", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-skip2-"));
    fs.mkdirSync(path.join(root, "incomplete"), { recursive: true });
    expect(buildState(root).epics).toEqual({});
  });

  it("parses depends-on and blocks frontmatter arrays", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-dep-"));
    const epicDir = path.join(root, "epic1");
    fs.mkdirSync(epicDir, { recursive: true });
    fs.writeFileSync(
      path.join(epicDir, "_epic.md"),
      `---
id: epic1
title: epic1
status: todo
---
`,
    );
    const story1Dir = path.join(epicDir, "story1");
    fs.mkdirSync(story1Dir, { recursive: true });
    fs.writeFileSync(
      path.join(story1Dir, "_story.md"),
      `---
id: story1
title: story1
status: todo
depends-on: []
blocks: [story2, other-epic/x]
---
`,
    );
    const story2Dir = path.join(epicDir, "story2");
    fs.mkdirSync(story2Dir, { recursive: true });
    fs.writeFileSync(
      path.join(story2Dir, "_story.md"),
      `---
id: story2
title: story2
status: todo
depends-on: [story1]
blocks: []
---
`,
    );

    const state = buildState(root);
    expect(state.epics.epic1.stories.story1.depends_on).toEqual([]);
    expect(state.epics.epic1.stories.story1.blocks).toEqual([
      "story2",
      "other-epic/x",
    ]);
    expect(state.epics.epic1.stories.story2.depends_on).toEqual(["story1"]);
    expect(state.epics.epic1.stories.story2.blocks).toEqual([]);
  });
});

describe("countTaskCheckboxes", () => {
  it("counts both unchecked and checked", () => {
    const content = `## Tasks
- [x] done one
- [ ] open one
- [X] capital X also counts
`;
    expect(countTaskCheckboxes(content)).toEqual({ total: 3, completed: 2 });
  });

  it("only counts inside the ## Tasks section", () => {
    const content = `## Tasks
- [x] in tasks

## Notes
- [x] outside, should not count
`;
    expect(countTaskCheckboxes(content)).toEqual({ total: 1, completed: 1 });
  });

  it("returns zeros when no Tasks section exists", () => {
    expect(countTaskCheckboxes(`## Goal\n\nFoo\n`)).toEqual({
      total: 0,
      completed: 0,
    });
  });
});

describe("parseFrontmatter", () => {
  it("extracts simple string keys", () => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fm-"));
    const fp = path.join(dir, "f.md");
    fs.writeFileSync(
      fp,
      `---
id: x
title: A title
status: done
---

Body`,
    );
    expect(parseFrontmatter(fp)).toEqual({
      id: "x",
      title: "A title",
      status: "done",
    });
  });

  it("returns {} when no frontmatter present", () => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fm2-"));
    const fp = path.join(dir, "f.md");
    fs.writeFileSync(fp, "# Just a heading\n");
    expect(parseFrontmatter(fp)).toEqual({});
  });

  it("parses array frontmatter values", () => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fm-arr-"));
    const fp = path.join(dir, "f.md");
    fs.writeFileSync(
      fp,
      `---
id: x
items: [a, b, "c"]
empty: []
---
`,
    );
    expect(parseFrontmatter(fp)).toEqual({
      id: "x",
      items: ["a", "b", "c"],
      empty: [],
    });
  });
});

describe("computeReadyBlocked", () => {
  it("returns ready for stories whose depends_on are all done", () => {
    const state = {
      updated_at: "x",
      epics: {
        e: {
          status: "in-progress",
          title: "e",
          stories: {
            a: {
              status: "done",
              title: "a",
              ac_total: 0,
              ac_completed: 0,
              depends_on: [],
              blocks: [],
            },
            b: {
              status: "todo",
              title: "b",
              ac_total: 0,
              ac_completed: 0,
              depends_on: ["a"],
              blocks: [],
            },
          },
        },
      },
    };
    const { ready, blocked } = computeReadyBlocked(state);
    expect(ready).toEqual([{ epic: "e", story: "b", title: "b" }]);
    expect(blocked).toEqual([]);
  });

  it("returns blocked for stories whose depends_on are not done", () => {
    const state = {
      updated_at: "x",
      epics: {
        e: {
          status: "in-progress",
          title: "e",
          stories: {
            a: {
              status: "todo",
              title: "a",
              ac_total: 0,
              ac_completed: 0,
              depends_on: [],
              blocks: [],
            },
            b: {
              status: "todo",
              title: "b",
              ac_total: 0,
              ac_completed: 0,
              depends_on: ["a"],
              blocks: [],
            },
          },
        },
      },
    };
    const { ready, blocked } = computeReadyBlocked(state);
    expect(ready).toEqual([{ epic: "e", story: "a", title: "a" }]);
    expect(blocked).toEqual([
      { epic: "e", story: "b", title: "b", waiting_on: ["e/a"] },
    ]);
  });

  it("resolves cross-epic refs", () => {
    const state = {
      updated_at: "x",
      epics: {
        e1: {
          status: "done",
          title: "e1",
          stories: {
            a: {
              status: "done",
              title: "a",
              ac_total: 0,
              ac_completed: 0,
              depends_on: [],
              blocks: [],
            },
          },
        },
        e2: {
          status: "in-progress",
          title: "e2",
          stories: {
            b: {
              status: "todo",
              title: "b",
              ac_total: 0,
              ac_completed: 0,
              depends_on: ["e1/a"],
              blocks: [],
            },
          },
        },
      },
    };
    const { ready, blocked } = computeReadyBlocked(state);
    expect(ready).toEqual([{ epic: "e2", story: "b", title: "b" }]);
    expect(blocked).toEqual([]);
  });

  it("skips done stories", () => {
    const state = {
      updated_at: "x",
      epics: {
        e: {
          status: "done",
          title: "e",
          stories: {
            a: {
              status: "done",
              title: "a",
              ac_total: 1,
              ac_completed: 1,
              depends_on: [],
              blocks: [],
            },
          },
        },
      },
    };
    const { ready, blocked } = computeReadyBlocked(state);
    expect(ready).toEqual([]);
    expect(blocked).toEqual([]);
  });
});
