/**
 * Tests for .opencode/tools/create-issue.ts — the create-issue custom tool.
 *
 * Mirror of tests/test_pipeline_status_tool.ts / test_memory_setup_tool.ts:
 * the tool is a spawnSync wrapper around `gh issue create` with title/body
 * validation.
 *
 * Runtime note: opencode ships a standalone binary with Bun bundled inside;
 * there is no separate `bun` CLI on the host (CI runner uses node + pytest).
 * The CI runs the equivalent Python tests in tests/test_create_issue_tool.py
 * via the JS loader tests/_ts_loader.mjs (exec_stub_json mode for multi-arg
 * tools). This file documents the intended TS-side test cases and is
 * runnable under `bun test` once a bun runtime is available on the host.
 *
 * Test cases (mirror tests/test_create_issue_tool.py):
 *   - test_valid_issue — valid title + body → "Issue created: <url>"
 *   - test_title_too_long — title >80 chars → error
 *   - test_missing_kontekst — body missing ## Контекст → error
 *   - test_missing_zadacha — body missing ## Задача → error
 *   - test_missing_kriterii — body missing ## Критерии приемки → error
 *   - test_sdd_missing_contracts — body missing ## Контракты → error
 *   - test_sdd_missing_invariants — body missing ## Инварианты → error
 *   - test_sdd_missing_edge_cases — body missing ## Граничные случаи → error
 *   - test_sdd_missing_impact — body missing ## Влияние на связанные компоненты → error
 *   - test_sdd_impact_trivial_ok — 8th section 'нет связанных компонентов' → success
 *   - test_sdd_missing_out_of_scope — body missing ## Вне scope → error
 *   - test_latin_only_body — body without Cyrillic → error
 */

import { describe, test, expect, mock } from "bun:test" with { type: "'bun-test'" }
import { spawnSync } from "child_process"
import path from "path"

const TOOL_SRC = path.resolve(import.meta.dir, "..", ".opencode", "tools", "create-issue.ts")

function ctx() {
  return {
    sessionID: "t", messageID: "t", agent: "t",
    directory: ".", worktree: ".",
    abort: new AbortController().signal,
    metadata() {}, async ask() {},
  }
}

const VALID_BODY = "## Контекст\nНужен tool\n\n## Задача\nСоздать tool\n\n## Контракты\nTool валидирует 8 секций\n\n## Инварианты\nВсе секции обязательны\n\n## Граничные случаи\nНет секций → error\n\n## Влияние на связанные компоненты\nНет связанных компонентов (валидатор изолирован)\n\n## Вне scope\nИзменение gh CLI\n\n## Критерии приемки\nTool работает"

describe("create-issue tool", () => {
  test("test_valid_issue — valid title + body succeeds", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "https://github.com/x/y/issues/1\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): add create-issue tool",
      body: VALID_BODY,
    }, ctx())
    expect(result).toBe("Issue created: https://github.com/x/y/issues/1")
  })

  test("test_title_too_long — title >80 chars → error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const longDesc = "x".repeat(81)
    const result = await mod.default.execute({
      title: `feat(tools): ${longDesc}`,
      body: VALID_BODY,
    }, ctx())
    expect(result).toContain("must match format")
  })

  test("test_missing_kontekst — body missing ## Контекст → error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: "## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Влияние на связанные компоненты\nНет связанных компонентов\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
    }, ctx())
    expect(result).toContain("## Контекст")
  })

  test("test_missing_zadacha — body missing ## Задача → error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: "## Контекст\nКонтекст\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Влияние на связанные компоненты\nНет связанных компонентов\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
    }, ctx())
    expect(result).toContain("## Задача")
  })

  test("test_missing_kriterii — body missing ## Критерии приемки → error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Влияние на связанные компоненты\nНет связанных компонентов\n\n## Вне scope\nНе делаем",
    }, ctx())
    expect(result).toContain("## Критерии приемки")
  })

  test("test_sdd_missing_contracts — body missing ## Контракты → error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Влияние на связанные компоненты\nНет связанных компонентов\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
    }, ctx())
    expect(result).toContain("## Контракты")
  })

  test("test_sdd_missing_invariants — body missing ## Инварианты → error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Граничные случаи\nОшибки\n\n## Влияние на связанные компоненты\nНет связанных компонентов\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
    }, ctx())
    expect(result).toContain("## Инварианты")
  })

  test("test_sdd_missing_edge_cases — body missing ## Граничные случаи → error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Влияние на связанные компоненты\nНет связанных компонентов\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
    }, ctx())
    expect(result).toContain("## Граничные случаи")
  })

  test("test_sdd_missing_impact — body missing ## Влияние на связанные компоненты → error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Вне scope\nНе делаем\n\n## Критерии приемки\nГотово",
    }, ctx())
    expect(result).toContain("## Влияние на связанные компоненты")
  })

  test("test_sdd_impact_trivial_ok — 8th section 'нет связанных компонентов' → success", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "https://github.com/x/y/issues/1\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: VALID_BODY,
    }, ctx())
    expect(result).toBe("Issue created: https://github.com/x/y/issues/1")
  })

  test("test_sdd_missing_out_of_scope — body missing ## Вне scope → error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: "## Контекст\nКонтекст\n\n## Задача\nСделать\n\n## Контракты\nКонтракты\n\n## Инварианты\nПравила\n\n## Граничные случаи\nОшибки\n\n## Влияние на связанные компоненты\nНет связанных компонентов\n\n## Критерии приемки\nГотово",
    }, ctx())
    expect(result).toContain("## Вне scope")
  })

  test("test_latin_only_body — body without Cyrillic → error", async () => {
    // NOTE: spec validation order checks headings (## Контекст, ## Задача,
    // ## Критерии приемки) BEFORE the Cyrillic check. Since the headings
    // themselves are Cyrillic, a body that passes the heading checks always
    // passes the Cyrillic check. Therefore a Latin-only body (no Cyrillic)
    // also lacks the Russian headings and fails on the heading check first.
    // The Cyrillic check is effectively dead code given the heading checks —
    // documented as spec issue in handoff.
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 0, stdout: "url\n", stderr: "" }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: "## Context\nSome\n\n## Task\nDo it\n\n## Acceptance criteria\nDone",
    }, ctx())
    // Body lacks Russian headings → heading check fires (not Cyrillic check).
    expect(result).toContain("## Контекст")
  })

  test("test_repo_explicit — repo='foo/bar' prepends --repo to gh argv", async () => {
    let capturedArgs
    mock.module("child_process", () => ({
      spawnSync: (_cmd, args) => {
        capturedArgs = args
        return { status: 0, stdout: "https://github.com/foo/bar/issues/1\n", stderr: "" }
      },
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: VALID_BODY,
      repo: "foo/bar",
    }, ctx())
    expect(result).toBe("Issue created: https://github.com/foo/bar/issues/1")
    expect(capturedArgs[0]).toBe("--repo")
    expect(capturedArgs[1]).toBe("foo/bar")
    expect(capturedArgs[2]).toBe("issue")
  })

  test("test_repo_omitted — no --repo flag (auto-detect from cwd)", async () => {
    let capturedArgs
    mock.module("child_process", () => ({
      spawnSync: (_cmd, args) => {
        capturedArgs = args
        return { status: 0, stdout: "https://github.com/x/y/issues/1\n", stderr: "" }
      },
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    await mod.default.execute({
      title: "feat(tools): valid title",
      body: VALID_BODY,
    }, ctx())
    // Backward-compat: --repo MUST NOT be present when repo arg omitted.
    expect(capturedArgs).not.toContain("--repo")
    expect(capturedArgs[0]).toBe("issue")
  })

  test("test_repo_invalid — invalid repo + gh failure → tool error", async () => {
    mock.module("child_process", () => ({
      spawnSync: () => ({ status: 1, stdout: "", stderr: 'expected the "owner/repo" format' }),
    }))
    const mod = await import(TOOL_SRC + "?t=" + Date.now())
    const result = await mod.default.execute({
      title: "feat(tools): valid title",
      body: VALID_BODY,
      repo: "not-a-valid-repo",
    }, ctx())
    expect(result).toContain("gh issue create failed")
    expect(result).toContain("exit 1")
  })
})