"""Extended tests for hooks/installer.py module."""

import importlib
import os
import stat
from pathlib import Path

import pytest

installer = importlib.import_module("gjalla_precommit.hooks.installer")

# Commands used by the attestation guardrails
PRE_COMMIT_CMD = "bash scripts/gjalla-attestation-check.sh"
POST_COMMIT_CMD = "bash scripts/gjalla-post-commit-upload.sh"


class TestInstallStatus:
    """Tests for InstallStatus enum."""

    def test_install_status_values(self):
        """Should have expected status values."""
        assert installer.InstallStatus.INSTALLED.value == "installed"
        assert installer.InstallStatus.ALREADY_EXISTS.value == "already_exists"
        assert installer.InstallStatus.ADDED_TO_EXISTING.value == "added_to_existing"
        assert installer.InstallStatus.FAILED.value == "failed"
        assert installer.InstallStatus.SKIPPED.value == "skipped"


class TestIsGjallaInstalled:
    """Tests for is_gjalla_installed function."""

    def test_not_installed_no_hook(self, temp_repo):
        """Should return False when no pre-commit hook exists."""
        assert installer.is_gjalla_installed(temp_repo) is False

    def test_not_installed_other_hook(self, temp_repo):
        """Should return False when hook exists but no gjalla."""
        hooks_dir = temp_repo / ".git" / "hooks"
        hooks_dir.mkdir(parents=True, exist_ok=True)
        hook = hooks_dir / "pre-commit"
        hook.write_text("#!/bin/sh\necho 'other hook'\n")
        assert installer.is_gjalla_installed(temp_repo) is False

    def test_installed_with_marker(self, temp_repo):
        """Should return True when marker is present."""
        hooks_dir = temp_repo / ".git" / "hooks"
        hooks_dir.mkdir(parents=True, exist_ok=True)
        hook = hooks_dir / "pre-commit"
        hook.write_text(f"#!/bin/sh\n{installer.GJALLA_MARKER}\n{PRE_COMMIT_CMD}\n")
        assert installer.is_gjalla_installed(temp_repo) is True

    def test_installed_with_gjalla_precommit_string(self, temp_repo):
        """Should return True when 'gjalla-precommit' appears in hook content."""
        hooks_dir = temp_repo / ".git" / "hooks"
        hooks_dir.mkdir(parents=True, exist_ok=True)
        hook = hooks_dir / "pre-commit"
        hook.write_text("#!/bin/sh\n# installed by gjalla-precommit\nsome-command\n")
        assert installer.is_gjalla_installed(temp_repo) is True


class TestInstallHook:
    """Tests for install_hook function (pre-commit)."""

    def test_install_new_hook(self, temp_repo):
        """Should install new pre-commit hook."""
        result = installer.install_hook(temp_repo, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.INSTALLED
        assert result.hook_path is not None
        assert result.hook_path.exists()

        # Check content
        content = result.hook_path.read_text()
        assert "#!/bin/sh" in content
        assert installer.GJALLA_MARKER in content
        assert PRE_COMMIT_CMD in content

    @pytest.mark.skipif(os.name == "nt", reason="Windows has no S_IXUSR")
    def test_install_hook_sets_executable(self, temp_repo):
        """Should make hook executable."""
        result = installer.install_hook(temp_repo, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.INSTALLED

        # Check executable permission
        mode = result.hook_path.stat().st_mode
        assert mode & stat.S_IXUSR  # User execute

    def test_install_hook_already_exists_updates(self, temp_repo):
        """Should update existing hook on re-install."""
        # First install
        installer.install_hook(temp_repo, command=PRE_COMMIT_CMD)

        # Second install attempt — should update, not skip
        result = installer.install_hook(temp_repo, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.UPDATED

    def test_install_hook_prepends_to_existing(self, temp_repo):
        """Should prepend to existing hook without gjalla."""
        hooks_dir = temp_repo / ".git" / "hooks"
        hooks_dir.mkdir(parents=True, exist_ok=True)
        hook = hooks_dir / "pre-commit"
        hook.write_text("#!/bin/sh\necho 'existing hook'\nexit 0\n")

        result = installer.install_hook(temp_repo, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.ADDED_TO_EXISTING

        # Check content has both
        content = result.hook_path.read_text()
        assert installer.GJALLA_MARKER in content
        assert "existing hook" in content

    def test_install_hook_preserves_shebang(self, temp_repo):
        """Should preserve existing shebang."""
        hooks_dir = temp_repo / ".git" / "hooks"
        hooks_dir.mkdir(parents=True, exist_ok=True)
        hook = hooks_dir / "pre-commit"
        hook.write_text("#!/bin/bash\necho 'bash hook'\n")

        result = installer.install_hook(temp_repo, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.ADDED_TO_EXISTING

        content = result.hook_path.read_text()
        # Should preserve bash shebang
        assert "#!/bin/bash" in content

    def test_install_hook_no_git_dir(self, tmp_path):
        """Should fail when no .git directory."""
        result = installer.install_hook(tmp_path, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.FAILED
        assert "Not a git repository" in result.message

    def test_install_hook_no_command_skips(self, temp_repo):
        """Should return SKIPPED when no command is provided."""
        result = installer.install_hook(temp_repo)
        assert result.status == installer.InstallStatus.SKIPPED
        assert "No hook command" in result.message


class TestInstallPostCommitHook:
    """Tests for install_post_commit_hook function."""

    def test_install_new_post_commit(self, temp_repo):
        """Should install new post-commit hook."""
        result = installer.install_post_commit_hook(temp_repo, command=POST_COMMIT_CMD)
        assert result.status == installer.InstallStatus.INSTALLED
        assert result.hook_path is not None
        assert result.hook_path.exists()

        content = result.hook_path.read_text()
        assert "#!/bin/sh" in content
        assert installer.GJALLA_POST_COMMIT_MARKER in content
        assert POST_COMMIT_CMD in content

    @pytest.mark.skipif(os.name == "nt", reason="Windows has no S_IXUSR")
    def test_install_post_commit_sets_executable(self, temp_repo):
        """Should make hook executable."""
        result = installer.install_post_commit_hook(temp_repo, command=POST_COMMIT_CMD)
        assert result.status == installer.InstallStatus.INSTALLED

        mode = result.hook_path.stat().st_mode
        assert mode & stat.S_IXUSR

    def test_install_post_commit_already_exists_updates(self, temp_repo):
        """Should update existing hook on re-install."""
        installer.install_post_commit_hook(temp_repo, command=POST_COMMIT_CMD)
        result = installer.install_post_commit_hook(temp_repo, command=POST_COMMIT_CMD)
        assert result.status == installer.InstallStatus.UPDATED

    def test_install_post_commit_appends_to_existing(self, temp_repo):
        """Should append to existing hook."""
        hooks_dir = temp_repo / ".git" / "hooks"
        hooks_dir.mkdir(parents=True, exist_ok=True)
        hook = hooks_dir / "post-commit"
        hook.write_text("#!/bin/sh\necho 'existing post-commit'\n")

        result = installer.install_post_commit_hook(temp_repo, command=POST_COMMIT_CMD)
        assert result.status == installer.InstallStatus.ADDED_TO_EXISTING

        content = result.hook_path.read_text()
        assert installer.GJALLA_POST_COMMIT_MARKER in content
        assert "existing post-commit" in content

    def test_install_post_commit_no_git_dir(self, tmp_path):
        """Should fail when no .git directory."""
        result = installer.install_post_commit_hook(tmp_path, command=POST_COMMIT_CMD)
        assert result.status == installer.InstallStatus.FAILED

    def test_install_post_commit_no_command_skips(self, temp_repo):
        """Should return SKIPPED when no command is provided."""
        result = installer.install_post_commit_hook(temp_repo)
        assert result.status == installer.InstallStatus.SKIPPED


class TestInstallHooks:
    """Tests for install_hooks function (both hooks)."""

    def test_install_both_hooks(self, temp_repo):
        """Should install both pre-commit and post-commit hooks."""
        result = installer.install_hooks(
            temp_repo,
            pre_commit_command=PRE_COMMIT_CMD,
            post_commit_command=POST_COMMIT_CMD,
        )

        assert result.pre_commit.status == installer.InstallStatus.INSTALLED
        assert result.post_commit.status == installer.InstallStatus.INSTALLED

    def test_install_hooks_returns_both_results(self, temp_repo):
        """Should return results for both hooks."""
        result = installer.install_hooks(
            temp_repo,
            pre_commit_command=PRE_COMMIT_CMD,
            post_commit_command=POST_COMMIT_CMD,
        )

        assert hasattr(result, "pre_commit")
        assert hasattr(result, "post_commit")
        assert isinstance(result.pre_commit, installer.HookInstallResult)
        assert isinstance(result.post_commit, installer.HookInstallResult)

    def test_install_hooks_idempotent(self, temp_repo):
        """Second install should report already exists."""
        installer.install_hooks(
            temp_repo,
            pre_commit_command=PRE_COMMIT_CMD,
            post_commit_command=POST_COMMIT_CMD,
        )
        result = installer.install_hooks(
            temp_repo,
            pre_commit_command=PRE_COMMIT_CMD,
            post_commit_command=POST_COMMIT_CMD,
        )

        assert result.pre_commit.status == installer.InstallStatus.UPDATED
        assert result.post_commit.status == installer.InstallStatus.UPDATED

    def test_install_hooks_no_commands_skips_both(self, temp_repo):
        """Should skip both hooks when no commands provided."""
        result = installer.install_hooks(temp_repo)
        assert result.pre_commit.status == installer.InstallStatus.SKIPPED
        assert result.post_commit.status == installer.InstallStatus.SKIPPED


class TestHookInstallResult:
    """Tests for HookInstallResult dataclass."""

    def test_result_has_status(self):
        """Should have status field."""
        result = installer.HookInstallResult(
            status=installer.InstallStatus.INSTALLED,
            message="Success",
        )
        assert result.status == installer.InstallStatus.INSTALLED

    def test_result_has_message(self):
        """Should have message field."""
        result = installer.HookInstallResult(
            status=installer.InstallStatus.FAILED,
            message="Error occurred",
        )
        assert result.message == "Error occurred"

    def test_result_hook_path_optional(self):
        """hook_path should be optional."""
        result = installer.HookInstallResult(
            status=installer.InstallStatus.FAILED,
            message="No path",
        )
        assert result.hook_path is None


class TestHooksInstallResult:
    """Tests for HooksInstallResult dataclass."""

    def test_result_has_both_hooks(self):
        """Should have both hook results."""
        pre_commit = installer.HookInstallResult(
            status=installer.InstallStatus.INSTALLED,
            message="Pre-commit installed",
        )
        post_commit = installer.HookInstallResult(
            status=installer.InstallStatus.INSTALLED,
            message="Post-commit installed",
        )
        result = installer.HooksInstallResult(
            pre_commit=pre_commit,
            post_commit=post_commit,
        )
        assert result.pre_commit.status == installer.InstallStatus.INSTALLED
        assert result.post_commit.status == installer.InstallStatus.INSTALLED


class TestHookContentIntegrity:
    """Tests to verify hook content is correct."""

    def test_pre_commit_hook_content(self, temp_repo):
        """Pre-commit hook should have correct content."""
        result = installer.install_hook(temp_repo, command=PRE_COMMIT_CMD)
        content = result.hook_path.read_text()

        # Should have shebang
        assert content.startswith("#!/bin/sh")
        # Should have marker
        assert installer.GJALLA_MARKER in content
        # Should have command
        assert PRE_COMMIT_CMD in content

    def test_post_commit_hook_content(self, temp_repo):
        """Post-commit hook should have correct content."""
        result = installer.install_post_commit_hook(temp_repo, command=POST_COMMIT_CMD)
        content = result.hook_path.read_text()

        assert content.startswith("#!/bin/sh")
        assert installer.GJALLA_POST_COMMIT_MARKER in content
        assert POST_COMMIT_CMD in content

    def test_prepended_hook_preserves_order(self, temp_repo):
        """When prepending, gjalla should run before existing hook."""
        hooks_dir = temp_repo / ".git" / "hooks"
        hooks_dir.mkdir(parents=True, exist_ok=True)
        hook = hooks_dir / "pre-commit"
        hook.write_text("#!/bin/sh\necho 'original'\n")

        installer.install_hook(temp_repo, command=PRE_COMMIT_CMD)
        content = hook.read_text()
        lines = content.split("\n")

        # Find positions
        gjalla_pos = None
        original_pos = None
        for i, line in enumerate(lines):
            if PRE_COMMIT_CMD in line:
                gjalla_pos = i
            if "original" in line:
                original_pos = i

        # Gjalla should come before original
        assert gjalla_pos is not None
        assert original_pos is not None
        assert gjalla_pos < original_pos
