"""Tests for guardrails commands: check, setup, sync, installer, scripts."""

import json
import os
import stat
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch, mock_open

import pytest
from click.testing import CliRunner

from gjalla_precommit.commands.check import check, _get_staged_diff_hash, _parse_attestation
from gjalla_precommit.commands.setup import setup, _ensure_gitignore
from gjalla_precommit.commands.sync import _log_attestation
from gjalla_precommit.hooks.installer import (
    install_hook,
    install_post_commit_hook,
    install_hooks,
    is_gjalla_installed,
    InstallStatus,
    GJALLA_MARKER,
    GJALLA_POST_COMMIT_MARKER,
)
from gjalla_precommit.hooks.scripts import (
    attestation_pre_commit_script,
    post_commit_upload_script,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def git_repo(tmp_path):
    """Create a minimal git repo structure."""
    git_dir = tmp_path / ".git"
    git_dir.mkdir()
    hooks_dir = git_dir / "hooks"
    hooks_dir.mkdir()
    return tmp_path


@pytest.fixture
def gjalla_repo(git_repo):
    """Git repo with .gjalla/config.json."""
    config = {"project_id": 46, "api_url": "https://gjalla.io", "api_key_env": "GJALLA_API_KEY"}
    gjalla_dir = git_repo / ".gjalla"
    gjalla_dir.mkdir(exist_ok=True)
    (gjalla_dir / "config.json").write_text(json.dumps(config))
    return git_repo


# ===========================================================================
# check.py tests
# ===========================================================================

class TestCheckCommand:
    """Tests for gjalla check."""

    def test_passthrough_when_no_gjalla_config(self, git_repo, monkeypatch):
        """If .gjalla doesn't exist, check exits 0 silently."""
        monkeypatch.chdir(git_repo)
        runner = CliRunner()
        result = runner.invoke(check)
        assert result.exit_code == 0

    def test_skip_attestation_env_var(self, gjalla_repo, monkeypatch):
        """SKIP_ATTESTATION=1 skips the check."""
        monkeypatch.chdir(gjalla_repo)
        monkeypatch.setenv("SKIP_ATTESTATION", "1")
        runner = CliRunner()
        result = runner.invoke(check)
        assert result.exit_code == 0
        assert "skipped" in result.output.lower() or result.exit_code == 0

    def test_missing_attestation_file_fails(self, gjalla_repo, monkeypatch):
        """Missing .commit-attestation.md → exit 1 with instructions."""
        monkeypatch.chdir(gjalla_repo)
        monkeypatch.delenv("SKIP_ATTESTATION", raising=False)
        runner = CliRunner()

        with patch("gjalla_precommit.commands.check._get_staged_diff_hash", return_value="abc123def456"):
            result = runner.invoke(check)

        assert result.exit_code == 1
        assert "Attestation Required" in result.output
        assert "staged_diff_hash" in result.output

    def test_stale_attestation_fails(self, gjalla_repo, monkeypatch):
        """Attestation with wrong diff hash → exit 1."""
        monkeypatch.chdir(gjalla_repo)
        monkeypatch.delenv("SKIP_ATTESTATION", raising=False)

        attestation = gjalla_repo / ".commit-attestation.md"
        attestation.write_text(
            "---\n"
            "staged_diff_hash: old_hash_that_does_not_match\n"
            "timestamp: 2026-02-10T15:30:00Z\n"
            "agent: claude-code\n"
            "summary: Test\n"
            "---\n"
        )

        runner = CliRunner()
        with patch("gjalla_precommit.commands.check._get_staged_diff_hash", return_value="current_different_hash"):
            result = runner.invoke(check)

        assert result.exit_code == 1
        assert "stale" in result.output.lower()

    def test_valid_attestation_passes(self, gjalla_repo, monkeypatch):
        """Correct diff hash → exit 0."""
        monkeypatch.chdir(gjalla_repo)
        monkeypatch.delenv("SKIP_ATTESTATION", raising=False)

        the_hash = "a3f8c2e1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
        attestation = gjalla_repo / ".commit-attestation.md"
        attestation.write_text(
            f"---\n"
            f"staged_diff_hash: {the_hash}\n"
            f"timestamp: 2026-02-10T15:30:00Z\n"
            f"agent: claude-code\n"
            f"summary: Test\n"
            f"---\n"
        )

        runner = CliRunner()
        with patch("gjalla_precommit.commands.check._get_staged_diff_hash", return_value=the_hash):
            result = runner.invoke(check)

        assert result.exit_code == 0
        assert "verified" in result.output.lower()

    def test_missing_attestation_shows_example_reference(self, gjalla_repo, monkeypatch):
        """Instructions reference the example attestation file."""
        monkeypatch.chdir(gjalla_repo)
        monkeypatch.delenv("SKIP_ATTESTATION", raising=False)

        runner = CliRunner()
        with patch("gjalla_precommit.commands.check._get_staged_diff_hash", return_value="abc"):
            result = runner.invoke(check)

        assert "example-attestation.md" in result.output
        assert "SKIP_ATTESTATION" in result.output


class TestParseAttestation:
    """Tests for _parse_attestation helper."""

    def test_parses_yaml_frontmatter(self, tmp_path):
        content = (
            "---\n"
            "staged_diff_hash: abc123\n"
            "timestamp: 2026-02-10T15:30:00Z\n"
            "agent: claude-code\n"
            "agent_provider: anthropic\n"
            "agent_model: claude-opus-4-6\n"
            "summary: Test commit\n"
            "---\n"
        )
        f = tmp_path / "test.md"
        f.write_text(content)
        fields = _parse_attestation(f)
        assert fields["staged_diff_hash"] == "abc123"
        assert fields["agent"] == "claude-code"
        assert fields["agent_provider"] == "anthropic"
        assert fields["agent_model"] == "claude-opus-4-6"
        assert fields["summary"] == "Test commit"

    def test_ignores_comment_lines(self, tmp_path):
        content = "# This is a comment\nkey: value\n"
        f = tmp_path / "test.md"
        f.write_text(content)
        fields = _parse_attestation(f)
        assert "key" in fields
        assert fields["key"] == "value"

    def test_ignores_yaml_delimiters(self, tmp_path):
        content = "---\nkey: value\n---\n"
        f = tmp_path / "test.md"
        f.write_text(content)
        fields = _parse_attestation(f)
        assert fields["key"] == "value"

    def test_strips_quotes(self, tmp_path):
        content = 'key1: "quoted"\nkey2: \'single\'\n'
        f = tmp_path / "test.md"
        f.write_text(content)
        fields = _parse_attestation(f)
        assert fields["key1"] == "quoted"
        assert fields["key2"] == "single"

    def test_replaces_spaces_with_underscores_in_keys(self, tmp_path):
        content = "rules checked: true\n"
        f = tmp_path / "test.md"
        f.write_text(content)
        fields = _parse_attestation(f)
        assert "rules_checked" in fields

    def test_handles_colon_in_value(self, tmp_path):
        content = "timestamp: 2026-02-10T15:30:00Z\n"
        f = tmp_path / "test.md"
        f.write_text(content)
        fields = _parse_attestation(f)
        assert fields["timestamp"] == "2026-02-10T15:30:00Z"

    def test_empty_values_excluded(self, tmp_path):
        content = "key:\n"
        f = tmp_path / "test.md"
        f.write_text(content)
        fields = _parse_attestation(f)
        assert "key" not in fields


# ===========================================================================
# setup.py tests
# ===========================================================================

class TestSetupCommand:
    """Tests for gjalla setup."""

    def test_fails_without_gjalla_config(self, git_repo, monkeypatch):
        """setup requires .gjalla to exist."""
        monkeypatch.chdir(git_repo)
        runner = CliRunner()
        result = runner.invoke(setup)
        assert result.exit_code == 1
        assert "gjalla init" in result.output.lower()

    def test_creates_scripts_directory(self, gjalla_repo, monkeypatch):
        """setup creates scripts/ with attestation check and upload scripts."""
        monkeypatch.chdir(gjalla_repo)
        runner = CliRunner()
        result = runner.invoke(setup, ["--no-hooks"])
        assert result.exit_code == 0

        pre_commit = gjalla_repo / "scripts" / "gjalla-attestation-check.sh"
        post_commit = gjalla_repo / "scripts" / "gjalla-post-commit-upload.sh"
        assert pre_commit.exists()
        assert post_commit.exists()
        # Verify executable
        assert pre_commit.stat().st_mode & stat.S_IEXEC

    def test_scripts_contain_expected_content(self, gjalla_repo, monkeypatch):
        """Generated scripts contain attestation check logic."""
        monkeypatch.chdir(gjalla_repo)
        runner = CliRunner()
        runner.invoke(setup, ["--no-hooks"])

        pre_content = (gjalla_repo / "scripts" / "gjalla-attestation-check.sh").read_text()
        assert "GJALLA_CONFIG" in pre_content
        assert "SKIP_ATTESTATION" in pre_content
        assert "staged_diff_hash" in pre_content

        post_content = (gjalla_repo / "scripts" / "gjalla-post-commit-upload.sh").read_text()
        assert "gjalla sync --up" in post_content

    def test_updates_gitignore(self, gjalla_repo, monkeypatch):
        """setup adds attestation entries to .gitignore."""
        monkeypatch.chdir(gjalla_repo)
        runner = CliRunner()
        runner.invoke(setup, ["--no-hooks"])

        gitignore = (gjalla_repo / ".gitignore").read_text()
        assert ".commit-attestation.md" in gitignore
        assert ".gjallalog" in gitignore

    def test_gitignore_idempotent(self, gjalla_repo, monkeypatch):
        """Running setup twice doesn't duplicate .gitignore entries."""
        monkeypatch.chdir(gjalla_repo)
        (gjalla_repo / ".gitignore").write_text(".commit-attestation.md\n.gjallalog\n")

        runner = CliRunner()
        runner.invoke(setup, ["--no-hooks"])

        gitignore = (gjalla_repo / ".gitignore").read_text()
        assert gitignore.count(".commit-attestation.md") == 1
        assert gitignore.count(".gjallalog") == 1

    def test_installs_hooks_by_default(self, gjalla_repo, monkeypatch):
        """setup --hooks installs pre-commit and post-commit hooks."""
        monkeypatch.chdir(gjalla_repo)
        runner = CliRunner()
        result = runner.invoke(setup)
        assert result.exit_code == 0

        pre_hook = gjalla_repo / ".git" / "hooks" / "pre-commit"
        post_hook = gjalla_repo / ".git" / "hooks" / "post-commit"
        assert pre_hook.exists()
        assert post_hook.exists()
        assert "gjalla-attestation-check.sh" in pre_hook.read_text()
        assert "gjalla-post-commit-upload.sh" in post_hook.read_text()

    def test_no_hooks_flag_skips_hook_installation(self, gjalla_repo, monkeypatch):
        """setup --no-hooks creates scripts but skips hooks."""
        monkeypatch.chdir(gjalla_repo)
        runner = CliRunner()
        result = runner.invoke(setup, ["--no-hooks"])
        assert result.exit_code == 0

        pre_hook = gjalla_repo / ".git" / "hooks" / "pre-commit"
        assert not pre_hook.exists()

    def test_fails_with_invalid_gjalla_config(self, git_repo, monkeypatch):
        """setup fails if .gjalla/config.json is not valid JSON."""
        monkeypatch.chdir(git_repo)
        gjalla_dir = git_repo / ".gjalla"
        gjalla_dir.mkdir(exist_ok=True)
        (gjalla_dir / "config.json").write_text("not valid json{{{")
        runner = CliRunner()
        result = runner.invoke(setup)
        assert result.exit_code == 1


class TestEnsureGitignore:
    """Tests for _ensure_gitignore helper."""

    def test_creates_gitignore_if_missing(self, tmp_path):
        _ensure_gitignore(tmp_path, [".commit-attestation.md"])
        content = (tmp_path / ".gitignore").read_text()
        assert ".commit-attestation.md" in content

    def test_appends_to_existing(self, tmp_path):
        (tmp_path / ".gitignore").write_text("node_modules\n")
        _ensure_gitignore(tmp_path, [".gjallalog"])
        content = (tmp_path / ".gitignore").read_text()
        assert "node_modules" in content
        assert ".gjallalog" in content

    def test_no_duplicates(self, tmp_path):
        (tmp_path / ".gitignore").write_text(".gjallalog\n")
        _ensure_gitignore(tmp_path, [".gjallalog"])
        content = (tmp_path / ".gitignore").read_text()
        assert content.count(".gjallalog") == 1


# ===========================================================================
# sync.py tests
# ===========================================================================

class TestLogAttestation:
    """Tests for _log_attestation helper in sync.py."""

    def test_parses_attestation_and_appends_jsonl(self, tmp_path):
        """_log_attestation reads attestation fields and writes JSONL to gjallalog."""
        attestation_path = tmp_path / ".commit-attestation.md"
        attestation_path.write_text(
            "---\n"
            "staged_diff_hash: abc123\n"
            "timestamp: 2026-02-10T15:30:00Z\n"
            "agent: claude-code\n"
            "agent_provider: anthropic\n"
            "agent_model: claude-opus-4-6\n"
            "rules.checked: true\n"
            "services_affected: web-api,auth-service\n"
            "architecture.changes: false\n"
            "summary: Added rate limiting\n"
            "---\n"
        )
        gjallalog_path = tmp_path / ".gjallalog"

        with patch("subprocess.run") as mock_run:
            mock_run.side_effect = [
                MagicMock(stdout="abc123def456\n", returncode=0),  # git rev-parse HEAD
                MagicMock(stdout="feature/auth\n", returncode=0),  # git rev-parse --abbrev-ref HEAD
            ]
            _log_attestation(attestation_path, gjallalog_path)

        assert gjallalog_path.exists()
        entry = json.loads(gjallalog_path.read_text().strip())
        assert entry["ts"] == "2026-02-10T15:30:00Z"
        assert entry["commit"] == "abc123def456"
        assert entry["branch"] == "feature/auth"
        assert entry["agent"] == "claude-code"
        assert entry["agent_provider"] == "anthropic"
        assert entry["agent_model"] == "claude-opus-4-6"
        assert entry["rules_checked"] is True
        assert entry["services"] == ["web-api", "auth-service"]
        assert entry["arch_change"] is False
        assert entry["summary"] == "Added rate limiting"
        assert entry["uploaded"] is False

    def test_appends_to_existing_log(self, tmp_path):
        """_log_attestation appends to existing .gjallalog."""
        gjallalog_path = tmp_path / ".gjallalog"
        gjallalog_path.write_text('{"ts":"old","uploaded":true}\n')

        attestation_path = tmp_path / ".commit-attestation.md"
        attestation_path.write_text("summary: New entry\n")

        with patch("subprocess.run") as mock_run:
            mock_run.side_effect = [
                MagicMock(stdout="hash\n", returncode=0),
                MagicMock(stdout="main\n", returncode=0),
            ]
            _log_attestation(attestation_path, gjallalog_path)

        lines = gjallalog_path.read_text().strip().splitlines()
        assert len(lines) == 2
        assert json.loads(lines[0])["ts"] == "old"
        assert json.loads(lines[1])["summary"] == "New entry"

    def test_handles_missing_fields(self, tmp_path):
        """_log_attestation handles attestation with minimal fields."""
        attestation_path = tmp_path / ".commit-attestation.md"
        attestation_path.write_text("summary: Minimal\n")
        gjallalog_path = tmp_path / ".gjallalog"

        with patch("subprocess.run") as mock_run:
            mock_run.side_effect = [
                MagicMock(stdout="hash\n", returncode=0),
                MagicMock(stdout="dev\n", returncode=0),
            ]
            _log_attestation(attestation_path, gjallalog_path)

        entry = json.loads(gjallalog_path.read_text().strip())
        assert entry["ts"] == ""
        assert entry["agent"] == ""
        assert entry["agent_provider"] == ""
        assert entry["agent_model"] == ""
        assert entry["branch"] == "dev"
        assert entry["rules_checked"] is False
        assert entry["services"] == []
        assert entry["tech"] == []
        assert entry["arch_change"] is False
        assert entry["summary"] == "Minimal"

    def test_handles_git_failure(self, tmp_path):
        """_log_attestation uses 'unknown' when git rev-parse fails."""
        attestation_path = tmp_path / ".commit-attestation.md"
        attestation_path.write_text("summary: No git\n")
        gjallalog_path = tmp_path / ".gjallalog"

        with patch("subprocess.run") as mock_run:
            mock_run.side_effect = [
                MagicMock(stdout="", returncode=128),
                MagicMock(stdout="", returncode=128),
            ]
            _log_attestation(attestation_path, gjallalog_path)

        entry = json.loads(gjallalog_path.read_text().strip())
        assert entry["commit"] == "unknown"
        assert entry["branch"] == "unknown"


# ===========================================================================
# installer.py tests
# ===========================================================================

class TestInstallHook:
    """Tests for pre-commit hook installation."""

    def test_installs_new_hook(self, git_repo):
        result = install_hook(git_repo, command="bash scripts/check.sh")
        assert result.status == InstallStatus.INSTALLED
        hook = git_repo / ".git" / "hooks" / "pre-commit"
        assert hook.exists()
        content = hook.read_text()
        assert GJALLA_MARKER in content
        assert "bash scripts/check.sh" in content
        assert hook.stat().st_mode & stat.S_IEXEC

    def test_adds_to_existing_hook(self, git_repo):
        hook = git_repo / ".git" / "hooks" / "pre-commit"
        hook.write_text("#!/bin/sh\nlint-staged\n")
        hook.chmod(0o755)

        result = install_hook(git_repo, command="bash scripts/check.sh")
        assert result.status == InstallStatus.ADDED_TO_EXISTING
        content = hook.read_text()
        assert "lint-staged" in content
        assert GJALLA_MARKER in content

    def test_detects_already_installed(self, git_repo):
        hook = git_repo / ".git" / "hooks" / "pre-commit"
        hook.write_text(f"#!/bin/sh\n{GJALLA_MARKER}\nbash scripts/check.sh\n")

        result = install_hook(git_repo, command="bash scripts/check.sh")
        assert result.status == InstallStatus.ALREADY_EXISTS

    def test_skips_when_no_command(self, git_repo):
        result = install_hook(git_repo)
        assert result.status == InstallStatus.SKIPPED

    def test_fails_when_no_git_dir(self, tmp_path):
        result = install_hook(tmp_path, command="bash scripts/check.sh")
        assert result.status == InstallStatus.FAILED


class TestInstallPostCommitHook:
    """Tests for post-commit hook installation."""

    def test_installs_new_hook(self, git_repo):
        result = install_post_commit_hook(git_repo, command="bash scripts/upload.sh")
        assert result.status == InstallStatus.INSTALLED
        hook = git_repo / ".git" / "hooks" / "post-commit"
        assert hook.exists()
        content = hook.read_text()
        assert GJALLA_POST_COMMIT_MARKER in content
        assert "bash scripts/upload.sh" in content

    def test_appends_to_existing_hook(self, git_repo):
        hook = git_repo / ".git" / "hooks" / "post-commit"
        hook.write_text("#!/bin/sh\nnotify-slack\n")
        hook.chmod(0o755)

        result = install_post_commit_hook(git_repo, command="bash scripts/upload.sh")
        assert result.status == InstallStatus.ADDED_TO_EXISTING
        content = hook.read_text()
        assert "notify-slack" in content
        assert GJALLA_POST_COMMIT_MARKER in content

    def test_detects_already_installed(self, git_repo):
        hook = git_repo / ".git" / "hooks" / "post-commit"
        hook.write_text(f"#!/bin/sh\n{GJALLA_POST_COMMIT_MARKER}\nbash scripts/upload.sh\n")

        result = install_post_commit_hook(git_repo, command="bash scripts/upload.sh")
        assert result.status == InstallStatus.ALREADY_EXISTS

    def test_skips_when_no_command(self, git_repo):
        result = install_post_commit_hook(git_repo)
        assert result.status == InstallStatus.SKIPPED


class TestInstallHooks:
    """Tests for combined hooks installation."""

    def test_installs_both_hooks(self, git_repo):
        result = install_hooks(
            git_repo,
            pre_commit_command="bash scripts/check.sh",
            post_commit_command="bash scripts/upload.sh",
        )
        assert result.pre_commit.status == InstallStatus.INSTALLED
        assert result.post_commit.status == InstallStatus.INSTALLED

    def test_skips_both_when_no_commands(self, git_repo):
        result = install_hooks(git_repo)
        assert result.pre_commit.status == InstallStatus.SKIPPED
        assert result.post_commit.status == InstallStatus.SKIPPED


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

    def test_returns_false_when_no_hook(self, git_repo):
        assert not is_gjalla_installed(git_repo)

    def test_returns_true_when_marker_present(self, git_repo):
        hook = git_repo / ".git" / "hooks" / "pre-commit"
        hook.write_text(f"#!/bin/sh\n{GJALLA_MARKER}\nbash check.sh\n")
        assert is_gjalla_installed(git_repo)

    def test_returns_true_when_gjalla_precommit_in_content(self, git_repo):
        hook = git_repo / ".git" / "hooks" / "pre-commit"
        hook.write_text("#!/bin/sh\ngjalla-precommit run\n")
        assert is_gjalla_installed(git_repo)

    def test_returns_false_for_unrelated_hook(self, git_repo):
        hook = git_repo / ".git" / "hooks" / "pre-commit"
        hook.write_text("#!/bin/sh\nlint-staged\n")
        assert not is_gjalla_installed(git_repo)


# ===========================================================================
# scripts.py tests
# ===========================================================================

class TestBashScriptTemplates:
    """Tests for bash script templates."""

    def test_pre_commit_script_is_valid_bash(self):
        script = attestation_pre_commit_script()
        assert script.startswith("#!/usr/bin/env bash")
        assert "set -euo pipefail" in script

    def test_pre_commit_script_checks_gjalla_config(self):
        script = attestation_pre_commit_script()
        assert 'GJALLA_CONFIG=".gjalla/config.json"' in script
        assert "exit 0" in script  # passthrough when no config

    def test_pre_commit_script_supports_skip_env_var(self):
        script = attestation_pre_commit_script()
        assert "SKIP_ATTESTATION" in script

    def test_pre_commit_script_validates_diff_hash(self):
        script = attestation_pre_commit_script()
        assert "staged_diff_hash" in script
        assert "shasum -a 256" in script

    def test_post_commit_script_prefers_cli(self):
        script = post_commit_upload_script()
        assert "gjalla sync --up" in script

    def test_post_commit_script_has_bash_fallback(self):
        script = post_commit_upload_script()
        assert ".gjallalog" in script
        assert "git rev-parse HEAD" in script
        assert "git rev-parse --abbrev-ref HEAD" in script

    def test_post_commit_script_extracts_agent_identity(self):
        script = post_commit_upload_script()
        assert "agent_provider" in script
        assert "agent_model" in script

    def test_post_commit_script_cleans_up_attestation(self):
        script = post_commit_upload_script()
        assert 'rm -f "$ATTESTATION"' in script
