"""Integration tests for hook installer against real-world project setups.

Covers: Husky v7/v9, Lefthook, pre-commit framework, simple-git-hooks,
plain git repos, and end-to-end hook execution.
"""

import importlib
import os
import stat
import subprocess
from pathlib import Path

import pytest

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

PRE_COMMIT_CMD = "gjalla check --pre-commit"
POST_COMMIT_CMD = "gjalla upload --post-commit"


def _run_git(repo_path: Path, *args: str) -> str:
    result = subprocess.run(
        ["git", *args],
        cwd=repo_path,
        check=True,
        capture_output=True,
        text=True,
    )
    return result.stdout.strip()


# ---------------------------------------------------------------------------
# Fixtures — each simulates a real project hook setup
# ---------------------------------------------------------------------------


@pytest.fixture()
def husky_v7_repo(temp_repo: Path) -> Path:
    """Excalidraw / most JS/TS projects — Husky v7.

    core.hooksPath=.husky, existing pre-commit with commented-out lint-staged.
    """
    husky_dir = temp_repo / ".husky"
    husky_dir.mkdir()
    (husky_dir / "_").mkdir()
    (husky_dir / "_" / "husky.sh").write_text(
        '#!/usr/bin/env sh\n'
        '. "$(dirname -- "$0")/_/husky.sh"\n',
    )

    pre_commit = husky_dir / "pre-commit"
    pre_commit.write_text(
        "#!/usr/bin/env sh\n"
        '. "$(dirname -- "$0")/_/husky.sh"\n'
        "\n"
        "# yarn lint-staged\n",
    )
    pre_commit.chmod(0o755)

    _run_git(temp_repo, "config", "core.hooksPath", ".husky")
    return temp_repo


@pytest.fixture()
def husky_v9_repo(temp_repo: Path) -> Path:
    """Modern JS projects — Husky v9.

    core.hooksPath=.husky, pre-commit sources husky.sh then runs lint-staged.
    """
    husky_dir = temp_repo / ".husky"
    husky_dir.mkdir()
    (husky_dir / "_").mkdir()
    (husky_dir / "_" / "husky.sh").write_text(
        '#!/usr/bin/env sh\n'
        'if [ -z "$husky_skip_init" ]; then\n'
        '  echo "husky - running hook"\n'
        "fi\n",
    )

    pre_commit = husky_dir / "pre-commit"
    pre_commit.write_text(
        "#!/usr/bin/env sh\n"
        '. "$(dirname -- "$0")/_/husky.sh"\n'
        "\n"
        "npx lint-staged\n",
    )
    pre_commit.chmod(0o755)

    _run_git(temp_repo, "config", "core.hooksPath", ".husky")
    return temp_repo


@pytest.fixture()
def lefthook_repo(temp_repo: Path) -> Path:
    """Go/Rust/polyglot projects using Lefthook.

    No core.hooksPath, .git/hooks/pre-commit calls lefthook binary.
    """
    hooks_dir = temp_repo / ".git" / "hooks"
    hooks_dir.mkdir(parents=True, exist_ok=True)

    pre_commit = hooks_dir / "pre-commit"
    pre_commit.write_text(
        "#!/bin/sh\n"
        "\n"
        'if [ -x "$(command -v lefthook)" ]; then\n'
        "  exec lefthook run pre-commit\n"
        "fi\n",
    )
    pre_commit.chmod(0o755)

    # lefthook config at repo root
    (temp_repo / "lefthook.yml").write_text(
        "pre-commit:\n"
        "  commands:\n"
        "    lint:\n"
        "      run: golangci-lint run\n",
    )
    return temp_repo


@pytest.fixture()
def precommit_framework_repo(temp_repo: Path) -> Path:
    """Python projects using pre-commit.com framework.

    .git/hooks/pre-commit is the framework's runner script.
    """
    hooks_dir = temp_repo / ".git" / "hooks"
    hooks_dir.mkdir(parents=True, exist_ok=True)

    pre_commit = hooks_dir / "pre-commit"
    pre_commit.write_text(
        "#!/usr/bin/env bash\n"
        '# File generated by pre-commit: https://pre-commit.com\n'
        '# ID: 138fd403232d2ddd5efb44317e38bf03\n'
        "\n"
        "# start templated\n"
        'INSTALL_PYTHON=/usr/bin/python3\n'
        "ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)\n"
        '# end templated\n'
        "\n"
        'exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}" -- "$@"\n',
    )
    pre_commit.chmod(0o755)

    (temp_repo / ".pre-commit-config.yaml").write_text(
        "repos:\n"
        "  - repo: https://github.com/pre-commit/pre-commit-hooks\n"
        "    rev: v4.5.0\n"
        "    hooks:\n"
        "      - id: trailing-whitespace\n",
    )
    return temp_repo


@pytest.fixture()
def simple_git_hooks_repo(temp_repo: Path) -> Path:
    """Lightweight JS projects using simple-git-hooks.

    No core.hooksPath, hooks written directly to .git/hooks/.
    """
    hooks_dir = temp_repo / ".git" / "hooks"
    hooks_dir.mkdir(parents=True, exist_ok=True)

    pre_commit = hooks_dir / "pre-commit"
    pre_commit.write_text(
        "#!/bin/sh\n"
        "npx lint-staged\n",
    )
    pre_commit.chmod(0o755)

    (temp_repo / "package.json").write_text(
        '{"simple-git-hooks": {"pre-commit": "npx lint-staged"}}\n',
    )
    return temp_repo


@pytest.fixture()
def plain_repo_with_hooks(temp_repo: Path) -> Path:
    """Manual/custom hook setup — custom hooks in .git/hooks/."""
    hooks_dir = temp_repo / ".git" / "hooks"
    hooks_dir.mkdir(parents=True, exist_ok=True)

    pre_commit = hooks_dir / "pre-commit"
    pre_commit.write_text(
        "#!/bin/sh\n"
        "# Custom project checks\n"
        "make lint\n"
        "make typecheck\n",
    )
    pre_commit.chmod(0o755)

    post_commit = hooks_dir / "post-commit"
    post_commit.write_text(
        "#!/bin/sh\n"
        "echo 'Commit recorded'\n",
    )
    post_commit.chmod(0o755)
    return temp_repo


@pytest.fixture()
def plain_repo_no_hooks(temp_repo: Path) -> Path:
    """Fresh project, no hooks at all."""
    hooks_dir = temp_repo / ".git" / "hooks"
    # Remove sample hooks if git init created them
    if hooks_dir.exists():
        for f in hooks_dir.iterdir():
            f.unlink()
    return temp_repo


# ---------------------------------------------------------------------------
# Test classes
# ---------------------------------------------------------------------------


class TestHuskyV7Scenario:
    """JS/TS projects using Husky v7 (e.g. Excalidraw, Next.js apps)."""

    def test_pre_commit_installed_in_husky_dir(self, husky_v7_repo):
        result = installer.install_hook(husky_v7_repo, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.ADDED_TO_EXISTING
        assert result.hook_path == husky_v7_repo / ".husky" / "pre-commit"

    def test_post_commit_created_in_husky_dir(self, husky_v7_repo):
        result = installer.install_post_commit_hook(husky_v7_repo, command=POST_COMMIT_CMD)
        assert result.status == installer.InstallStatus.INSTALLED
        assert result.hook_path == husky_v7_repo / ".husky" / "post-commit"
        assert result.hook_path.exists()

    def test_existing_pre_commit_content_preserved(self, husky_v7_repo):
        installer.install_hook(husky_v7_repo, command=PRE_COMMIT_CMD)
        content = (husky_v7_repo / ".husky" / "pre-commit").read_text()
        assert "# yarn lint-staged" in content
        assert '. "$(dirname -- "$0")/_/husky.sh"' in content

    @pytest.mark.skipif(os.name == "nt", reason="Windows has no S_IXUSR")
    def test_hooks_are_executable(self, husky_v7_repo):
        installer.install_hooks(
            husky_v7_repo,
            pre_commit_command=PRE_COMMIT_CMD,
            post_commit_command=POST_COMMIT_CMD,
        )
        for name in ("pre-commit", "post-commit"):
            hook = husky_v7_repo / ".husky" / name
            assert hook.stat().st_mode & stat.S_IXUSR

    def test_core_hooks_path_unchanged(self, husky_v7_repo):
        installer.install_hooks(
            husky_v7_repo,
            pre_commit_command=PRE_COMMIT_CMD,
            post_commit_command=POST_COMMIT_CMD,
        )
        hooks_path = _run_git(husky_v7_repo, "config", "core.hooksPath")
        assert hooks_path == ".husky"


class TestHuskyV9Scenario:
    """Modern JS projects using Husky v9."""

    def test_husky_init_sourcing_preserved(self, husky_v9_repo):
        installer.install_hook(husky_v9_repo, command=PRE_COMMIT_CMD)
        content = (husky_v9_repo / ".husky" / "pre-commit").read_text()
        assert '. "$(dirname -- "$0")/_/husky.sh"' in content

    def test_gjalla_prepended_before_lint_staged(self, husky_v9_repo):
        installer.install_hook(husky_v9_repo, command=PRE_COMMIT_CMD)
        content = (husky_v9_repo / ".husky" / "pre-commit").read_text()
        lines = content.split("\n")

        gjalla_pos = next(i for i, l in enumerate(lines) if PRE_COMMIT_CMD in l)
        lint_pos = next(i for i, l in enumerate(lines) if "npx lint-staged" in l)
        assert gjalla_pos < lint_pos

    def test_post_commit_created_in_husky_dir(self, husky_v9_repo):
        result = installer.install_post_commit_hook(husky_v9_repo, command=POST_COMMIT_CMD)
        assert result.status == installer.InstallStatus.INSTALLED
        assert result.hook_path == husky_v9_repo / ".husky" / "post-commit"


class TestLefthookScenario:
    """Go/Rust/polyglot projects using Lefthook."""

    def test_installs_to_git_hooks(self, lefthook_repo):
        result = installer.install_hook(lefthook_repo, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.ADDED_TO_EXISTING
        assert result.hook_path == lefthook_repo / ".git" / "hooks" / "pre-commit"

    def test_gjalla_prepended_before_lefthook(self, lefthook_repo):
        installer.install_hook(lefthook_repo, command=PRE_COMMIT_CMD)
        content = (lefthook_repo / ".git" / "hooks" / "pre-commit").read_text()
        lines = content.split("\n")

        gjalla_pos = next(i for i, l in enumerate(lines) if PRE_COMMIT_CMD in l)
        lefthook_pos = next(i for i, l in enumerate(lines) if "lefthook" in l)
        assert gjalla_pos < lefthook_pos

    def test_both_hooks_installed(self, lefthook_repo):
        result = installer.install_hooks(
            lefthook_repo,
            pre_commit_command=PRE_COMMIT_CMD,
            post_commit_command=POST_COMMIT_CMD,
        )
        assert result.pre_commit.status == installer.InstallStatus.ADDED_TO_EXISTING
        assert result.post_commit.status == installer.InstallStatus.INSTALLED


class TestPreCommitFrameworkScenario:
    """Python projects using pre-commit.com framework."""

    def test_gjalla_prepended_to_framework_script(self, precommit_framework_repo):
        result = installer.install_hook(precommit_framework_repo, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.ADDED_TO_EXISTING

    def test_framework_content_preserved(self, precommit_framework_repo):
        installer.install_hook(precommit_framework_repo, command=PRE_COMMIT_CMD)
        content = (precommit_framework_repo / ".git" / "hooks" / "pre-commit").read_text()
        assert "pre-commit.com" in content
        assert "-mpre_commit" in content
        assert "INSTALL_PYTHON" in content

    def test_post_commit_created_alongside_framework(self, precommit_framework_repo):
        result = installer.install_post_commit_hook(
            precommit_framework_repo, command=POST_COMMIT_CMD
        )
        assert result.status == installer.InstallStatus.INSTALLED
        assert result.hook_path.exists()


class TestSimpleGitHooksScenario:
    """Lightweight JS projects using simple-git-hooks."""

    def test_installs_to_git_hooks(self, simple_git_hooks_repo):
        result = installer.install_hook(simple_git_hooks_repo, command=PRE_COMMIT_CMD)
        assert result.status == installer.InstallStatus.ADDED_TO_EXISTING
        assert result.hook_path == simple_git_hooks_repo / ".git" / "hooks" / "pre-commit"

    def test_existing_hook_content_preserved(self, simple_git_hooks_repo):
        installer.install_hook(simple_git_hooks_repo, command=PRE_COMMIT_CMD)
        content = (simple_git_hooks_repo / ".git" / "hooks" / "pre-commit").read_text()
        assert "npx lint-staged" in content
        assert installer.GJALLA_MARKER in content


class TestPlainGitScenario:
    """Repos with no hook manager."""

    def test_fresh_repo_creates_hooks_dir_and_both_hooks(self, plain_repo_no_hooks):
        result = installer.install_hooks(
            plain_repo_no_hooks,
            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
        assert result.pre_commit.hook_path.exists()
        assert result.post_commit.hook_path.exists()

    def test_existing_custom_hooks_preserved(self, plain_repo_with_hooks):
        installer.install_hooks(
            plain_repo_with_hooks,
            pre_commit_command=PRE_COMMIT_CMD,
            post_commit_command=POST_COMMIT_CMD,
        )
        pre = (plain_repo_with_hooks / ".git" / "hooks" / "pre-commit").read_text()
        assert "make lint" in pre
        assert "make typecheck" in pre
        assert installer.GJALLA_MARKER in pre

        post = (plain_repo_with_hooks / ".git" / "hooks" / "post-commit").read_text()
        assert "Commit recorded" in post
        assert installer.GJALLA_POST_COMMIT_MARKER in post

    def test_gjalla_prepended_in_pre_commit(self, plain_repo_with_hooks):
        installer.install_hook(plain_repo_with_hooks, command=PRE_COMMIT_CMD)
        content = (plain_repo_with_hooks / ".git" / "hooks" / "pre-commit").read_text()
        lines = content.split("\n")

        gjalla_pos = next(i for i, l in enumerate(lines) if PRE_COMMIT_CMD in l)
        make_pos = next(i for i, l in enumerate(lines) if "make lint" in l)
        assert gjalla_pos < make_pos


class TestEndToEndHookExecution:
    """Verify hooks actually run during git commit."""

    def test_pre_commit_hook_executes(self, temp_repo):
        marker_file = temp_repo / "pre-commit-ran.txt"
        cmd = f'touch "{marker_file}"'
        installer.install_hook(temp_repo, command=cmd)

        # Create a change and commit
        (temp_repo / "file.txt").write_text("hello\n")
        _run_git(temp_repo, "add", "file.txt")
        _run_git(temp_repo, "commit", "-m", "test commit")

        assert marker_file.exists()

    def test_post_commit_hook_executes(self, temp_repo):
        marker_file = temp_repo / "post-commit-ran.txt"
        cmd = f'touch "{marker_file}"'
        installer.install_post_commit_hook(temp_repo, command=cmd)

        (temp_repo / "file.txt").write_text("hello\n")
        _run_git(temp_repo, "add", "file.txt")
        _run_git(temp_repo, "commit", "-m", "test commit")

        assert marker_file.exists()

    def test_both_hooks_execute(self, temp_repo):
        pre_marker = temp_repo / "pre-ran.txt"
        post_marker = temp_repo / "post-ran.txt"

        installer.install_hooks(
            temp_repo,
            pre_commit_command=f'touch "{pre_marker}"',
            post_commit_command=f'touch "{post_marker}"',
        )

        (temp_repo / "file.txt").write_text("hello\n")
        _run_git(temp_repo, "add", "file.txt")
        _run_git(temp_repo, "commit", "-m", "test commit")

        assert pre_marker.exists()
        assert post_marker.exists()
