"""Tests for the hooks module: discovery, execution, augmentation, fail-safety."""

import io
import json
import os
import stat
import sys
import textwrap
from pathlib import Path

import pytest

import hooks
from conftest import PROJECT_ID


# ═══════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════

def _make_hook(project_dir: Path, name: str, content: str, executable: bool = False) -> Path:
    """Create a hook script under .autobuilder/hooks/."""
    hooks_dir = project_dir / ".autobuilder" / "hooks"
    hooks_dir.mkdir(parents=True, exist_ok=True)
    path = hooks_dir / name
    path.write_text(content, encoding="utf-8")
    if executable:
        path.chmod(path.stat().st_mode | stat.S_IEXEC)
    return path


def _log_fh() -> io.StringIO:
    return io.StringIO()


def _base_env(project_dir: Path) -> dict:
    return hooks.build_hook_env(
        project_dir=project_dir,
        project_id=PROJECT_ID,
    )


# ═══════════════════════════════════════════════════════════════════════════
# find_hook
# ═══════════════════════════════════════════════════════════════════════════


class TestFindHook:
    def test_returns_none_when_no_hooks_dir(self, project_dir: Path):
        assert hooks.find_hook(project_dir, "on-start") is None

    def test_returns_none_when_hooks_dir_empty(self, project_dir: Path):
        (project_dir / ".autobuilder" / "hooks").mkdir(parents=True)
        assert hooks.find_hook(project_dir, "on-start") is None

    def test_finds_bare_file(self, project_dir: Path):
        _make_hook(project_dir, "on-start", "#!/bin/bash\nexit 0", executable=True)
        found = hooks.find_hook(project_dir, "on-start")
        assert found is not None
        assert found.name == "on-start"

    def test_finds_py_file(self, project_dir: Path):
        _make_hook(project_dir, "on-start.py", "import sys; sys.exit(0)")
        found = hooks.find_hook(project_dir, "on-start")
        assert found is not None
        assert found.name == "on-start.py"

    def test_finds_sh_file(self, project_dir: Path):
        _make_hook(project_dir, "on-start.sh", "exit 0")
        found = hooks.find_hook(project_dir, "on-start")
        assert found is not None
        assert found.name == "on-start.sh"

    def test_bare_takes_priority_over_py(self, project_dir: Path):
        _make_hook(project_dir, "on-start", "#!/bin/bash\nexit 0", executable=True)
        _make_hook(project_dir, "on-start.py", "import sys; sys.exit(0)")
        found = hooks.find_hook(project_dir, "on-start")
        assert found.name == "on-start"

    def test_py_takes_priority_over_sh(self, project_dir: Path):
        _make_hook(project_dir, "on-start.py", "import sys; sys.exit(0)")
        _make_hook(project_dir, "on-start.sh", "exit 0")
        found = hooks.find_hook(project_dir, "on-start")
        assert found.name == "on-start.py"

    def test_does_not_match_wrong_name(self, project_dir: Path):
        _make_hook(project_dir, "on-stop.py", "import sys; sys.exit(0)")
        assert hooks.find_hook(project_dir, "on-start") is None


# ═══════════════════════════════════════════════════════════════════════════
# build_hook_env
# ═══════════════════════════════════════════════════════════════════════════


class TestBuildHookEnv:
    def test_always_sets_project_dir_and_id(self, project_dir: Path):
        env = hooks.build_hook_env(
            project_dir=project_dir,
            project_id="my-app",
        )
        assert env["AUTOBUILDER_PROJECT_DIR"] == str(project_dir)
        assert env["AUTOBUILDER_PROJECT_ID"] == "my-app"

    def test_optional_fields_absent_when_none(self, project_dir: Path):
        env = hooks.build_hook_env(
            project_dir=project_dir,
            project_id="my-app",
        )
        assert "AUTOBUILDER_PORT" not in env
        assert "AUTOBUILDER_ITERATION" not in env
        assert "AUTOBUILDER_PHASES" not in env
        assert "AUTOBUILDER_EXECUTOR" not in env
        assert "AUTOBUILDER_MODEL" not in env

    def test_sets_all_optional_fields(self, project_dir: Path):
        env = hooks.build_hook_env(
            project_dir=project_dir,
            project_id="my-app",
            port=5200,
            iteration=42,
            phases=["review", "execute"],
            executor="cursor",
            model="claude-4",
            agent_exit_code=0,
            newly_executed=["0001-setup", "0002-auth"],
            push_branch="main",
            error_message="something broke",
            consecutive_failures=3,
        )
        assert env["AUTOBUILDER_PORT"] == "5200"
        assert env["AUTOBUILDER_ITERATION"] == "42"
        assert env["AUTOBUILDER_PHASES"] == "review,execute"
        assert env["AUTOBUILDER_EXECUTOR"] == "cursor"
        assert env["AUTOBUILDER_MODEL"] == "claude-4"
        assert env["AUTOBUILDER_AGENT_EXIT_CODE"] == "0"
        assert env["AUTOBUILDER_NEWLY_EXECUTED"] == "0001-setup,0002-auth"
        assert env["AUTOBUILDER_PUSH_BRANCH"] == "main"
        assert env["AUTOBUILDER_ERROR"] == "something broke"
        assert env["AUTOBUILDER_CONSECUTIVE_FAILURES"] == "3"

    def test_inherits_parent_env(self, project_dir: Path):
        env = hooks.build_hook_env(
            project_dir=project_dir,
            project_id="my-app",
        )
        assert "PATH" in env


# ═══════════════════════════════════════════════════════════════════════════
# run_hook
# ═══════════════════════════════════════════════════════════════════════════


class TestRunHook:
    def test_returns_none_when_hook_absent(self, project_dir: Path):
        result = hooks.run_hook(
            project_dir, "on-start", _log_fh(),
            env=_base_env(project_dir),
        )
        assert result is None

    def test_runs_py_hook_success(self, project_dir: Path):
        _make_hook(project_dir, "on-start.py", textwrap.dedent("""\
            import sys
            print("hook ran")
            sys.exit(0)
        """))
        fh = _log_fh()
        result = hooks.run_hook(
            project_dir, "on-start", fh,
            env=_base_env(project_dir),
        )
        assert result is not None
        assert result.ok
        assert result.exit_code == 0
        assert "hook ran" in result.stdout
        assert "succeeded" in fh.getvalue()

    def test_runs_py_hook_failure(self, project_dir: Path):
        _make_hook(project_dir, "on-start.py", textwrap.dedent("""\
            import sys
            print("error details", file=sys.stderr)
            sys.exit(1)
        """))
        fh = _log_fh()
        result = hooks.run_hook(
            project_dir, "on-start", fh,
            env=_base_env(project_dir),
        )
        assert result is not None
        assert not result.ok
        assert result.exit_code == 1
        assert "error details" in result.stderr
        assert "failed" in fh.getvalue()

    def test_runs_sh_hook(self, project_dir: Path):
        _make_hook(project_dir, "on-start.sh", 'echo "shell hook"')
        result = hooks.run_hook(
            project_dir, "on-start", _log_fh(),
            env=_base_env(project_dir),
        )
        assert result is not None
        assert result.ok
        assert "shell hook" in result.stdout

    def test_receives_env_vars(self, project_dir: Path):
        _make_hook(project_dir, "on-start.py", textwrap.dedent("""\
            import os
            print(os.environ.get("AUTOBUILDER_PROJECT_ID", "MISSING"))
        """))
        result = hooks.run_hook(
            project_dir, "on-start", _log_fh(),
            env=_base_env(project_dir),
        )
        assert PROJECT_ID in result.stdout

    def test_receives_stdin(self, project_dir: Path):
        _make_hook(project_dir, "augment-review.py", textwrap.dedent("""\
            import sys, json
            data = json.load(sys.stdin)
            print(f"got iteration {data['iteration']}")
        """))
        result = hooks.run_hook(
            project_dir, "augment-review", _log_fh(),
            env=_base_env(project_dir),
            stdin_data=json.dumps({"iteration": 42}),
        )
        assert result.ok
        assert "got iteration 42" in result.stdout

    def test_timeout_returns_exit_124(self, project_dir: Path):
        _make_hook(project_dir, "on-start.py", textwrap.dedent("""\
            import time
            time.sleep(60)
        """))
        result = hooks.run_hook(
            project_dir, "on-start", _log_fh(),
            env=_base_env(project_dir),
            timeout=1,
        )
        assert result is not None
        assert result.exit_code == 124
        assert "timed out" in result.stderr.lower()

    def test_cwd_is_project_dir(self, project_dir: Path):
        _make_hook(project_dir, "on-start.py", textwrap.dedent("""\
            import os
            print(os.getcwd())
        """))
        result = hooks.run_hook(
            project_dir, "on-start", _log_fh(),
            env=_base_env(project_dir),
        )
        assert result.ok
        assert str(project_dir) in result.stdout

    def test_logs_stderr_lines(self, project_dir: Path):
        _make_hook(project_dir, "on-start.py", textwrap.dedent("""\
            import sys
            print("line1", file=sys.stderr)
            print("line2", file=sys.stderr)
        """))
        fh = _log_fh()
        hooks.run_hook(
            project_dir, "on-start", fh,
            env=_base_env(project_dir),
        )
        log_output = fh.getvalue()
        assert "[on-start stderr] line1" in log_output
        assert "[on-start stderr] line2" in log_output


# ═══════════════════════════════════════════════════════════════════════════
# run_augment_hook
# ═══════════════════════════════════════════════════════════════════════════


class TestRunAugmentHook:
    def test_returns_empty_when_absent(self, project_dir: Path):
        text = hooks.run_augment_hook(
            project_dir, "augment-review", _log_fh(),
            env=_base_env(project_dir),
            context={"iteration": 1},
        )
        assert text == ""

    def test_returns_stdout_on_success(self, project_dir: Path):
        _make_hook(project_dir, "augment-review.py", textwrap.dedent("""\
            import sys, json
            ctx = json.load(sys.stdin)
            print(f"Review iteration {ctx['iteration']}")
            print("Check accessibility scores")
        """))
        text = hooks.run_augment_hook(
            project_dir, "augment-review", _log_fh(),
            env=_base_env(project_dir),
            context={"iteration": 5},
        )
        assert "Review iteration 5" in text
        assert "Check accessibility scores" in text

    def test_returns_empty_on_failure(self, project_dir: Path):
        _make_hook(project_dir, "augment-plan.py", textwrap.dedent("""\
            import sys
            sys.exit(1)
        """))
        fh = _log_fh()
        text = hooks.run_augment_hook(
            project_dir, "augment-plan", fh,
            env=_base_env(project_dir),
            context={"iteration": 1},
        )
        assert text == ""
        assert "failed" in fh.getvalue()

    def test_strips_whitespace(self, project_dir: Path):
        _make_hook(project_dir, "augment-execute.py", textwrap.dedent("""\
            print("  some text  ")
            print("")
        """))
        text = hooks.run_augment_hook(
            project_dir, "augment-execute", _log_fh(),
            env=_base_env(project_dir),
            context={},
        )
        assert text == "some text"

    def test_returns_empty_on_timeout(self, project_dir: Path):
        _make_hook(project_dir, "augment-review.py", textwrap.dedent("""\
            import time
            time.sleep(60)
        """))
        text = hooks.run_augment_hook(
            project_dir, "augment-review", _log_fh(),
            env=_base_env(project_dir),
            context={"iteration": 1},
        )
        assert text == ""

    def test_context_passed_as_json(self, project_dir: Path):
        _make_hook(project_dir, "augment-review.py", textwrap.dedent("""\
            import sys, json
            ctx = json.load(sys.stdin)
            assert "project_id" in ctx
            assert "phases" in ctx
            print("validated")
        """))
        text = hooks.run_augment_hook(
            project_dir, "augment-review", _log_fh(),
            env=_base_env(project_dir),
            context={"project_id": "test", "phases": ["review"]},
        )
        assert text == "validated"


# ═══════════════════════════════════════════════════════════════════════════
# HookResult
# ═══════════════════════════════════════════════════════════════════════════


class TestHookResult:
    def test_ok_when_zero(self):
        r = hooks.HookResult(name="test", exit_code=0, stdout="out", stderr="")
        assert r.ok is True

    def test_not_ok_when_nonzero(self):
        r = hooks.HookResult(name="test", exit_code=1, stdout="", stderr="err")
        assert r.ok is False

    def test_frozen(self):
        r = hooks.HookResult(name="test", exit_code=0)
        with pytest.raises(AttributeError):
            r.exit_code = 1  # type: ignore[misc]


# ═══════════════════════════════════════════════════════════════════════════
# Integration: hooks work with no hooks directory at all
# ═══════════════════════════════════════════════════════════════════════════


class TestNoHooksDirectory:
    """All hook operations must succeed gracefully with no hooks dir."""

    def test_find_hook_no_dir(self, tmp_path: Path):
        assert hooks.find_hook(tmp_path, "on-start") is None

    def test_run_hook_no_dir(self, tmp_path: Path):
        result = hooks.run_hook(
            tmp_path, "on-start", _log_fh(),
            env=hooks.build_hook_env(project_dir=tmp_path, project_id="x"),
        )
        assert result is None

    def test_run_augment_hook_no_dir(self, tmp_path: Path):
        text = hooks.run_augment_hook(
            tmp_path, "augment-review", _log_fh(),
            env=hooks.build_hook_env(project_dir=tmp_path, project_id="x"),
            context={"iteration": 1},
        )
        assert text == ""

    def test_no_autobuilder_dir_at_all(self, tmp_path: Path):
        bare = tmp_path / "bare-project"
        bare.mkdir()
        assert hooks.find_hook(bare, "post-agent") is None
        result = hooks.run_hook(
            bare, "post-agent", _log_fh(),
            env=hooks.build_hook_env(project_dir=bare, project_id="x"),
        )
        assert result is None


# ═══════════════════════════════════════════════════════════════════════════
# HOOK_NAMES constant
# ═══════════════════════════════════════════════════════════════════════════


class TestHookNames:
    def test_contains_all_11_hooks(self):
        assert len(hooks.HOOK_NAMES) == 11

    def test_all_have_timeouts(self):
        for name in hooks.HOOK_NAMES:
            assert name in hooks.DEFAULT_TIMEOUTS

    def test_expected_hooks_present(self):
        expected = {
            "on-start", "on-stop", "pre-iteration",
            "augment-review", "augment-plan", "augment-execute",
            "post-agent", "pre-push", "post-push",
            "on-idle", "on-error",
        }
        assert set(hooks.HOOK_NAMES) == expected
