import { readFileSync } from "fs"
import path from "path"
import { describe, expect, test } from "bun:test"

const SKILL_PATH = path.join(
  process.cwd(),
  "plugins/compound-engineering/skills/ce-plan/SKILL.md",
)
const SKILL_BODY = readFileSync(SKILL_PATH, "utf8")

const HANDOFF_PATH = path.join(
  process.cwd(),
  "plugins/compound-engineering/skills/ce-plan/references/plan-handoff.md",
)
const HANDOFF_BODY = readFileSync(HANDOFF_PATH, "utf8")

// Regression guard for https://github.com/EveryInc/compound-engineering-plugin/issues/714.
//
// ce-plan Phase 5.4 presents a 4-option post-generation menu. Because SKILL.md
// content caches at session start while reference files load on demand, the
// per-option routing (what action fires when the user picks an option) MUST
// live in SKILL.md itself — not solely in references/plan-handoff.md. The
// reference may still hold elaborate sub-flows (HITL state machine, Issue
// Creation tracker detection); only the bare per-option action must be inline.
//
// Symptom when this regresses: the agent renders the menu, the user picks
// "Start `/ce-work` (Recommended)", and the agent stops in prose without
// invoking the ce-work skill.
describe("ce-plan post-generation menu routing", () => {
  test("SKILL.md contains inline routing for all four menu options", () => {
    // Anchor on the Phase 5.4 region so a stray match elsewhere in the file
    // doesn't satisfy these assertions.
    const phaseStart = SKILL_BODY.indexOf("##### 5.3.8")
    expect(
      phaseStart,
      "ce-plan SKILL.md no longer contains the '##### 5.3.8' phase heading — the test anchor needs updating, or Phase 5.4 was removed.",
    ).toBeGreaterThan(-1)
    const phaseRegion = SKILL_BODY.slice(phaseStart)

    // Each menu option must have a routing bullet in the phase region that
    // pairs the label with an action statement. The routing bullet shape is
    // `- **<label-or-label-fragment>** — <action sentence>`. We accept "—",
    // "->", or "-" as the separator so legitimate phrasing tweaks don't break
    // the test, but require:
    //   - the line starts with `- **` (a bullet, not the numbered menu list)
    //   - the bold span contains a fragment unique to the option label
    //   - the bold span is followed by a separator and at least one action verb
    // Testing for a label fragment (not the full label) tolerates the long
    // "Open in Proof (web app) — review and comment to iterate with the agent"
    // label without the assertion becoming brittle.
    const optionFragments: { name: string; fragment: string }[] = [
      { name: "Start /ce-work", fragment: "Start `/ce-work`" },
      { name: "Create Issue", fragment: "Create Issue" },
      { name: "Open in Proof", fragment: "Open in Proof" },
      // "Open in browser" is the HTML-mode replacement for Open in Proof.
      // Both routings must be present inline: the menu shows one or the other
      // depending on OUTPUT_FORMAT, so the agent needs both bullets to route
      // correctly without loading the reference.
      { name: "Open in browser", fragment: "Open in browser" },
      { name: "Done for now", fragment: "Done for now" },
    ]

    for (const { name, fragment } of optionFragments) {
      const escaped = fragment.replace(/[.*+?^${}()|[\]\\`]/g, "\\$&")
      // Bullet form: `- **...<fragment>...**` followed by separator + action,
      // ALL on the same line. Use `[ \t]*` for the inter-token gaps instead of
      // `\s*` so a bullet with no action text cannot match by spilling into the
      // next bullet's leading `-` (Codex P2 catch on PR #715: `\s*` consumed
      // newlines, letting an empty-action bullet pass the regex). The trailing
      // `[^\n]+` requires at least one non-newline character of action text.
      const inlineRoutingPattern = new RegExp(
        `^- \\*\\*[^\\n]*${escaped}[^\\n]*\\*\\*[ \\t]*(?:[—\\-]+>?|->)[ \\t]*[^\\n]+`,
        "m",
      )
      const found = inlineRoutingPattern.test(phaseRegion)
      expect(
        found,
        `ce-plan SKILL.md Phase 5.4 is missing inline routing for menu option "${name}". The bare per-option action MUST live in SKILL.md (not solely in references/plan-handoff.md) so an agent that doesn't load the reference still routes correctly. See https://github.com/EveryInc/compound-engineering-plugin/issues/714 and docs/solutions/skill-design/post-menu-routing-belongs-inline-2026-04-28.md.`,
      ).toBe(true)
    }
  })

  test("Start /ce-work routing names skill-invocation primitive and plan path", () => {
    const phaseStart = SKILL_BODY.indexOf("##### 5.3.8")
    const phaseRegion = SKILL_BODY.slice(phaseStart)

    // The Start /ce-work routing line must be platform-explicit. We require
    // that the routing BULLET (not the menu list entry) names both
    // (a) the skill-invocation primitive (Skill tool / skill-invocation /
    // skill primitive) and (b) the plan path being passed as the argument.
    // This is what makes the difference between "tell the user to type
    // /ce-work" and "fire the Skill tool now."
    //
    // Anchor on the bullet form `- **Start \`/ce-work\`**` to avoid matching
    // the numbered menu list entry `1. **Start \`/ce-work\`** (recommended) -`,
    // which legitimately doesn't carry the routing language.
    const ceWorkRoutingMatch = phaseRegion.match(
      /^- \*\*Start `\/ce-work`\*\*[\s\S]{0,500}/m,
    )
    expect(
      ceWorkRoutingMatch,
      "ce-plan SKILL.md Phase 5.4 is missing the inline '- **Start `/ce-work`** ...' routing bullet (distinct from the numbered menu list entry).",
    ).not.toBeNull()
    const block = ceWorkRoutingMatch![0]

    expect(
      /skill[\s-]?invocation|Skill tool|skill primitive/i.test(block),
      "ce-plan SKILL.md 'Start /ce-work' routing must name the skill-invocation primitive (e.g., 'Skill tool in Claude Code', 'platform's skill-invocation primitive') so the agent fires the invocation rather than announcing a handoff in prose. See issue #714.",
    ).toBe(true)

    expect(
      /plan path|plan file path|plan as the (?:skill )?argument|passing the plan/i.test(block),
      "ce-plan SKILL.md 'Start /ce-work' routing must name the plan path as the argument so the agent passes it correctly to ce-work. See issue #714.",
    ).toBe(true)
  })

  test("plan-handoff.md routing for Start /ce-work matches the inline platform-explicit phrasing", () => {
    // Both surfaces must converge: the reference file's routing line should
    // also use platform-explicit invocation language so that an agent which
    // does load the reference sees compatible (not contradictory) guidance.
    const ceWorkLine = HANDOFF_BODY.match(
      /\*\*Start `\/ce-work`\*\*[^\n]*->[^\n]+/,
    )
    expect(
      ceWorkLine,
      "references/plan-handoff.md is missing the routing line for 'Start /ce-work'.",
    ).not.toBeNull()

    expect(
      /skill[\s-]?invocation|Skill tool|skill primitive/i.test(ceWorkLine![0]),
      `references/plan-handoff.md 'Start /ce-work' routing must use platform-explicit invocation language matching SKILL.md (e.g., 'invoke the ce-work skill via the platform's skill-invocation primitive'). The bare 'Call /ce-work with the plan path' phrasing was the regression. Found: ${JSON.stringify(ceWorkLine![0])}`,
    ).toBe(true)
  })

  test("inline-routing regex rejects empty-action bullets even when followed by another bullet", () => {
    // Regression guard for Codex P2 finding on PR #715: the previous
    // `\s*(?:...)\s*` shape allowed newline consumption, so a bullet with no
    // action text on its own line could still match by spilling into the next
    // bullet's leading `-`. The first test in this file would silently pass
    // on a real regression. This test recreates the failure mode and asserts
    // the regex now refuses it.
    //
    // Construct the same regex shape used above and exercise it directly
    // against a hand-rolled fixture — no live SKILL.md needed.
    const fragment = "Start `/ce-work`"
    const escaped = fragment.replace(/[.*+?^${}()|[\]\\`]/g, "\\$&")
    const fixedRegex = new RegExp(
      `^- \\*\\*[^\\n]*${escaped}[^\\n]*\\*\\*[ \\t]*(?:[—\\-]+>?|->)[ \\t]*[^\\n]+`,
      "m",
    )
    const broken = [
      "- **Start `/ce-work`**",
      "- **Done for now** — End the turn.",
    ].join("\n")
    expect(
      fixedRegex.test(broken),
      "Routing regex must NOT match a bullet with no action text on its own line, even when the next bullet's `-` could be misread as the separator. If this assertion fires, the regex regressed back to consuming newlines (Codex P2 on PR #715).",
    ).toBe(false)

    // And confirm the regex still matches the legitimate same-line shape so
    // the negative case isn't masking a positive-case breakage.
    const valid = "- **Start `/ce-work`** — Invoke the ce-work skill, passing the plan path."
    expect(fixedRegex.test(valid)).toBe(true)
  })
})
