"""Tests for project actions API."""

from __future__ import annotations

import stat
import sys
from pathlib import Path

import pytest

from pygittools.actions import (
    Action,
    ActionSet,
    ActionSpec,
    ProjectActions,
    find_project_root,
    load_action_sets,
    load_action_specs,
    project_actions,
    project_venv_python,
    run_action_script,
)


def _write_pyproject(root: Path, actions_toml: str) -> None:
    (root / "pyproject.toml").write_text(
        f'[project]\nname = "demo"\nversion = "0"\n\n[tool.pgt]\nactions = {actions_toml}\n',
        encoding="utf-8",
    )


def _make_venv(root: Path) -> Path:
    venv = root / ".venv"
    bin_dir = venv / "bin"
    bin_dir.mkdir(parents=True)
    python = bin_dir / "python"
    python.symlink_to(sys.executable)
    return python


def test_find_project_root(tmp_path: Path) -> None:
    nested = tmp_path / "a" / "b"
    nested.mkdir(parents=True)
    (tmp_path / "pyproject.toml").write_text("[project]\nname='x'\n", encoding="utf-8")
    (nested / "actions.toml").write_text("[local]\nactions = []\n", encoding="utf-8")
    assert find_project_root(nested) == tmp_path.resolve()
    assert find_project_root(tmp_path) == tmp_path.resolve()


def test_find_project_root_actions_toml_only(tmp_path: Path) -> None:
    nested = tmp_path / "a" / "b"
    nested.mkdir(parents=True)
    (tmp_path / "actions.toml").write_text("[quality]\nactions = []\n", encoding="utf-8")
    assert find_project_root(nested) == tmp_path.resolve()


def test_load_action_specs_inline_tables(tmp_path: Path) -> None:
    _write_pyproject(
        tmp_path,
        """[
  { name = "lint", script = "scripts/lint.py" },
  { name = "test", script = "scripts/test.py" },
]""",
    )
    specs = load_action_specs(tmp_path)
    assert specs == [
        ActionSpec(name="lint", script="scripts/lint.py"),
        ActionSpec(name="test", script="scripts/test.py"),
    ]


def test_load_action_specs_pair_arrays(tmp_path: Path) -> None:
    _write_pyproject(tmp_path, '[["lint", "scripts/lint.py"], ["test", "scripts/test.sh"]]')
    specs = load_action_specs(tmp_path)
    assert [s.name for s in specs] == ["lint", "test"]


def test_load_action_specs_args(tmp_path: Path) -> None:
    _write_pyproject(
        tmp_path,
        '[{ name = "py", script = "scripts/pytest_version.py", args = ["3.12", "-q"] }]',
    )
    specs = load_action_specs(tmp_path)
    assert specs == [
        ActionSpec(name="py", script="scripts/pytest_version.py", args=("3.12", "-q")),
    ]


def test_workspace_python_version_set() -> None:
    root = Path(__file__).resolve().parents[3]
    sets = {item.name: item for item in load_action_sets(root)}
    versions = sets["testversions"]
    assert [spec.args for spec in versions.specs] == [("3.12",), ("3.13",), ("3.14",), ("3.15",)]
    assert {spec.script for spec in versions.specs} == {"scripts/pytest_version.py"}


def test_load_action_specs_invalid_args(tmp_path: Path) -> None:
    _write_pyproject(tmp_path, '[{ name = "py", script = "x.py", args = "3.12" }]')
    with pytest.raises(ValueError, match="args"):
        load_action_specs(tmp_path)


def test_run_action_script_passes_args(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    script = tmp_path / "echo_args.py"
    script.write_text("print('unused')\n", encoding="utf-8")
    bin_dir = tmp_path / ".venv" / "bin"
    win_dir = tmp_path / ".venv" / "Scripts"
    bin_dir.mkdir(parents=True)
    win_dir.mkdir()
    (bin_dir / "python").write_text("", encoding="utf-8")
    (win_dir / "python.exe").write_text("", encoding="utf-8")
    captured: list[list[str]] = []

    class _Proc:
        returncode = 0
        stdout = "ok"
        stderr = ""

    def fake_run(cmd: list[str], **kwargs: object) -> _Proc:
        del kwargs
        captured.append(list(cmd))
        return _Proc()

    monkeypatch.setattr("pygittools.actions.subprocess.run", fake_run)
    spec = ActionSpec(name="py", script="echo_args.py", args=("3.12", "-q"))
    ok, message = ProjectActions(tmp_path, [spec]).action("py")
    python = project_venv_python(tmp_path)
    assert python is not None
    assert ok is True
    assert message == "ok"
    assert captured == [[str(python), str(script.resolve()), "3.12", "-q"]]


def test_load_action_specs_invalid(tmp_path: Path) -> None:
    _write_pyproject(tmp_path, '{ name = "lint" }')
    with pytest.raises(ValueError, match="must be an array"):
        load_action_specs(tmp_path)


def test_load_action_sets_from_pyproject_and_actions_toml(tmp_path: Path) -> None:
    _write_pyproject(tmp_path, '[{ name = "lint", script = "scripts/lint.py" }]')
    pyproject = (tmp_path / "pyproject.toml").read_text(encoding="utf-8")
    (tmp_path / "pyproject.toml").write_text(
        pyproject
        + """
[tool.pgt.sets.release]
actions = [
  ["build", "scripts/build.py"],
]
""",
        encoding="utf-8",
    )
    (tmp_path / "actions.toml").write_text(
        """
name = "tools"

[[actions]]
name = "fmt"
script = "scripts/fmt.py"

[quality]
actions = [
  { name = "test", script = "scripts/test.py" },
]
""",
        encoding="utf-8",
    )
    assert load_action_sets(tmp_path) == [
        ActionSet(name="default", specs=(ActionSpec(name="lint", script="scripts/lint.py"),)),
        ActionSet(name="release", specs=(ActionSpec(name="build", script="scripts/build.py"),)),
        ActionSet(name="tools", specs=(ActionSpec(name="fmt", script="scripts/fmt.py"),)),
        ActionSet(name="quality", specs=(ActionSpec(name="test", script="scripts/test.py"),)),
    ]


def test_load_action_sets_named_pyproject_flat_set(tmp_path: Path) -> None:
    (tmp_path / "pyproject.toml").write_text(
        """
[tool.pgt]
name = "checks"
actions = [{ name = "lint", script = "lint.py" }]
""",
        encoding="utf-8",
    )
    sets = load_action_sets(tmp_path)
    assert [item.name for item in sets] == ["checks"]


def test_load_action_sets_duplicate_name(tmp_path: Path) -> None:
    _write_pyproject(tmp_path, '[{ name = "lint", script = "lint.py" }]')
    (tmp_path / "actions.toml").write_text(
        '[default]\nactions = [{ name = "other", script = "other.py" }]\n',
        encoding="utf-8",
    )
    with pytest.raises(ValueError, match="Duplicate action set name: default"):
        load_action_sets(tmp_path)


def test_load_action_sets_invalid_table(tmp_path: Path) -> None:
    (tmp_path / "actions.toml").write_text('quality = "nope"\n', encoding="utf-8")
    with pytest.raises(ValueError, match="unexpected key"):
        load_action_sets(tmp_path)


def test_project_actions_is_action(tmp_path: Path) -> None:
    _write_pyproject(tmp_path, '[{ name = "ok", script = "ok.py" }]')
    runner = project_actions(tmp_path)
    assert isinstance(runner, Action)
    assert issubclass(ProjectActions, Action)


@pytest.mark.skipif(sys.platform == "win32", reason="symlink venv setup requires SeCreateSymbolicLinkPrivilege")
def test_run_action_script_python_success(tmp_path: Path) -> None:
    _make_venv(tmp_path)
    script = tmp_path / "ok.py"
    script.write_text("print('hello-ok')\n", encoding="utf-8")
    ok, message = run_action_script(tmp_path, "ok.py")
    assert ok is True
    assert message == "hello-ok"


@pytest.mark.skipif(sys.platform == "win32", reason="symlink venv setup requires SeCreateSymbolicLinkPrivilege")
def test_run_action_script_python_failure(tmp_path: Path) -> None:
    _make_venv(tmp_path)
    script = tmp_path / "fail.py"
    script.write_text("import sys\nprint('boom', file=sys.stderr)\nsys.exit(2)\n", encoding="utf-8")
    ok, message = run_action_script(tmp_path, "fail.py")
    assert ok is False
    assert "boom" in message


def test_run_action_script_missing_venv(tmp_path: Path) -> None:
    script = tmp_path / "ok.py"
    script.write_text("print('x')\n", encoding="utf-8")
    ok, message = run_action_script(tmp_path, "ok.py")
    assert ok is False
    assert "venv not found" in message


@pytest.mark.skipif(sys.platform == "win32", reason="symlink venv setup requires SeCreateSymbolicLinkPrivilege")
def test_run_action_script_shell(tmp_path: Path) -> None:
    _make_venv(tmp_path)
    script = tmp_path / "hi.sh"
    script.write_text("#!/bin/bash\necho shell-ok\n", encoding="utf-8")
    script.chmod(script.stat().st_mode | stat.S_IXUSR)
    ok, message = run_action_script(tmp_path, "hi.sh")
    assert ok is True
    assert message == "shell-ok"
    assert project_venv_python(tmp_path) is not None


def test_project_actions_unknown(tmp_path: Path) -> None:
    _write_pyproject(tmp_path, "[]")
    ok, message = ProjectActions(tmp_path).action("missing")
    assert ok is False
    assert "Unknown action" in message


@pytest.mark.skipif(sys.platform == "win32", reason="symlink venv setup requires SeCreateSymbolicLinkPrivilege")
def test_project_actions_runs_named(tmp_path: Path) -> None:
    _make_venv(tmp_path)
    _write_pyproject(tmp_path, '[{ name = "greet", script = "greet.py" }]')
    (tmp_path / "greet.py").write_text("print('hi')\n", encoding="utf-8")
    ok, message = ProjectActions(tmp_path).action("greet")
    assert ok is True
    assert message == "hi"