"""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[1]
	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",)]
	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"