"""Tests for local-only mode: init branching prompt, show with local docs, sync no-op."""

import importlib
import json
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest
import yaml
from click.testing import CliRunner

init_module = importlib.import_module("gjalla_precommit.commands.init")
show_module = importlib.import_module("gjalla_precommit.commands.show")
sync_module = importlib.import_module("gjalla_precommit.commands.sync")
cli_module = importlib.import_module("gjalla_precommit.cli")
telemetry_module = importlib.import_module("gjalla_precommit.telemetry")


# ---------------------------------------------------------------------------
# Init: local-only flow
# ---------------------------------------------------------------------------

class TestInitLocalOnlyFlow:
    """Tests for the local-only branching path in gjalla init."""

    def test_no_at_branching_prompt_enters_local_flow(self, temp_repo, home_dir, monkeypatch):
        """Selecting 'No' at branching prompt → two path prompts → config has both keys."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)
        runner = CliRunner()

        arch_dir = temp_repo / "arch-docs"
        arch_dir.mkdir()
        (arch_dir / "arch.md").write_text("# Architecture\n")

        rules_dir = temp_repo / "rules-docs"
        rules_dir.mkdir()
        (rules_dir / "rules.md").write_text("# Rules\n")

        confirm_calls = []

        def mock_confirm_ask(prompt, **kwargs):
            confirm_calls.append(str(prompt))
            return False

        prompt_calls = []

        def mock_prompt_ask(prompt, **kwargs):
            prompt_calls.append(str(prompt))
            if "architecture" in str(prompt).lower():
                return str(arch_dir)
            if "rules" in str(prompt).lower():
                return str(rules_dir)
            return ""

        with patch.object(init_module, "load_global_config", return_value={"api_url": "https://gjalla.io", "projects": {}}), \
             patch.object(init_module, "save_global_config"), \
             patch.object(init_module, "is_git_repo", return_value=True), \
             patch.object(init_module, "Confirm") as mock_confirm, \
             patch.object(init_module, "Prompt") as mock_prompt:
            mock_confirm.ask.side_effect = mock_confirm_ask
            mock_prompt.ask.side_effect = mock_prompt_ask
            result = runner.invoke(cli_module.main, ["init"])

        assert result.exit_code == 0, result.output

        config = yaml.safe_load((temp_repo / ".gjalla" / "config.yaml").read_text())
        assert config["project_id"] is None
        assert config["local_arch_path"] == str(arch_dir)
        assert config["local_rules_path"] == str(rules_dir)
        assert "local_docs_path" not in config
        assert "local-only" in result.output.lower()

    def test_yes_flag_defaults_to_no_paths(self, temp_repo, home_dir, monkeypatch):
        """-y with no API key → no local paths set (skips prompts)."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)
        runner = CliRunner()

        with patch.object(init_module, "load_global_config", return_value={"api_url": "https://gjalla.io", "projects": {}}), \
             patch.object(init_module, "save_global_config"), \
             patch.object(init_module, "is_git_repo", return_value=True):
            result = runner.invoke(cli_module.main, ["init", "-y"])

        assert result.exit_code == 0, result.output

        config = yaml.safe_load((temp_repo / ".gjalla" / "config.yaml").read_text())
        assert config["project_id"] is None
        assert "local_arch_path" not in config
        assert "local_rules_path" not in config
        assert "local-only" in result.output.lower()

    def test_api_key_flag_skips_branching_prompt(self, temp_repo, home_dir, monkeypatch):
        """--api-key flag goes straight to remote flow, no branching prompt."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)
        runner = CliRunner()

        with patch.object(init_module, "load_global_config", return_value={"api_url": "https://gjalla.io", "projects": {}}), \
             patch.object(init_module, "save_global_config"), \
             patch.object(init_module, "verify_api_key", return_value=(True, "")) as verify_mock, \
             patch.object(init_module, "is_git_repo", return_value=True), \
             patch.object(init_module, "Confirm") as mock_confirm:
            mock_confirm.ask.return_value = False
            result = runner.invoke(cli_module.main, ["init", "--api-key", "gj_test", "--project-id", "42"])

        assert result.exit_code == 0, result.output
        verify_mock.assert_called_once()

        config = yaml.safe_load((temp_repo / ".gjalla" / "config.yaml").read_text())
        assert config["project_id"] == 42
        assert "local_docs_path" not in config or config.get("local_docs_path") is None

    def test_invalid_arch_path_exits_with_error(self, temp_repo, home_dir, monkeypatch):
        """Invalid architecture docs path → exit with error."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)
        runner = CliRunner()

        with patch.object(init_module, "load_global_config", return_value={"api_url": "https://gjalla.io", "projects": {}}), \
             patch.object(init_module, "save_global_config"), \
             patch.object(init_module, "is_git_repo", return_value=True), \
             patch.object(init_module, "Confirm") as mock_confirm, \
             patch.object(init_module, "Prompt") as mock_prompt:
            mock_confirm.ask.return_value = False  # "No" at branching
            mock_prompt.ask.return_value = "/nonexistent/path/to/docs"
            result = runner.invoke(cli_module.main, ["init"])

        assert result.exit_code != 0
        assert "does not exist" in result.output.lower()

    def test_invalid_rules_path_exits_with_error(self, temp_repo, home_dir, monkeypatch):
        """Valid arch path but invalid rules path → exit with error."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)
        runner = CliRunner()

        arch_dir = temp_repo / "arch-docs"
        arch_dir.mkdir()

        prompt_calls = []

        def mock_prompt_ask(prompt, **kwargs):
            prompt_calls.append(str(prompt))
            if "architecture" in str(prompt).lower():
                return str(arch_dir)
            return "/nonexistent/rules/path"

        with patch.object(init_module, "load_global_config", return_value={"api_url": "https://gjalla.io", "projects": {}}), \
             patch.object(init_module, "save_global_config"), \
             patch.object(init_module, "is_git_repo", return_value=True), \
             patch.object(init_module, "Confirm") as mock_confirm, \
             patch.object(init_module, "Prompt") as mock_prompt:
            mock_confirm.ask.return_value = False
            mock_prompt.ask.side_effect = mock_prompt_ask
            result = runner.invoke(cli_module.main, ["init"])

        assert result.exit_code != 0
        assert "does not exist" in result.output.lower()

    def test_skip_both_paths_during_init(self, temp_repo, home_dir, monkeypatch):
        """Skipping both paths stores neither key, show commands print helpful message."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)
        runner = CliRunner()

        def mock_prompt_ask(prompt, **kwargs):
            return ""  # blank = skip

        with patch.object(init_module, "load_global_config", return_value={"api_url": "https://gjalla.io", "projects": {}}), \
             patch.object(init_module, "save_global_config"), \
             patch.object(init_module, "is_git_repo", return_value=True), \
             patch.object(init_module, "Confirm") as mock_confirm, \
             patch.object(init_module, "Prompt") as mock_prompt:
            mock_confirm.ask.return_value = False
            mock_prompt.ask.side_effect = mock_prompt_ask
            result = runner.invoke(cli_module.main, ["init"])

        assert result.exit_code == 0, result.output

        config = yaml.safe_load((temp_repo / ".gjalla" / "config.yaml").read_text())
        assert "local_arch_path" not in config
        assert "local_rules_path" not in config
        assert "no project linked" in result.output.lower()

    def test_yes_at_branching_then_no_api_key_errors(self, temp_repo, home_dir, monkeypatch):
        """Selecting 'Yes' at branching but providing empty API key → error."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)
        runner = CliRunner()

        confirm_calls = []

        def mock_confirm_ask(prompt, **kwargs):
            confirm_calls.append(str(prompt))
            if "remote" in str(prompt).lower():
                return True
            return False

        with patch.object(init_module, "load_global_config", return_value={"api_url": "https://gjalla.io", "projects": {}}), \
             patch.object(init_module, "save_global_config"), \
             patch.object(init_module, "is_git_repo", return_value=True), \
             patch.object(init_module, "Confirm") as mock_confirm, \
             patch.object(init_module, "Prompt") as mock_prompt:
            mock_confirm.ask.side_effect = mock_confirm_ask
            mock_prompt.ask.return_value = ""  # empty API key
            result = runner.invoke(cli_module.main, ["init"])

        assert result.exit_code != 0
        assert "api key is required" in result.output.lower()


# ---------------------------------------------------------------------------
# Show: local docs mode
# ---------------------------------------------------------------------------

class TestShowLocalDocs:
    """Tests for gjalla show subcommands in local-only mode."""

    def test_show_rules_with_local_file(self, temp_repo, home_dir, monkeypatch):
        """show rules with local_rules_path pointing to a file prints its content."""
        monkeypatch.chdir(temp_repo)
        rules_file = temp_repo / "my-rules.md"
        rules_file.write_text("# Rules\n\n- No spaghetti code\n")

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_rules_path": str(rules_file),
        }, default_flow_style=False))

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["show", "rules"])

        assert result.exit_code == 0, result.output
        assert "spaghetti" in result.output
        assert "gjalla connect" in result.output.lower() or "gjalla remote" in result.output.lower()

    def test_show_rules_with_local_directory(self, temp_repo, home_dir, monkeypatch):
        """show rules with local_rules_path pointing to a directory lists files."""
        monkeypatch.chdir(temp_repo)
        docs_dir = temp_repo / "rules-docs"
        docs_dir.mkdir()
        (docs_dir / "constraints.md").write_text("# Constraints\nMicroservices\n")
        (docs_dir / "rules.txt").write_text("Rule 1: Be nice\n")

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_rules_path": str(docs_dir),
        }, default_flow_style=False))

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["show", "rules"])

        assert result.exit_code == 0, result.output
        assert "Microservices" in result.output
        assert "Be nice" in result.output

    def test_show_arch_with_local_arch_path(self, temp_repo, home_dir, monkeypatch):
        """show arch reads local_arch_path, not local_rules_path."""
        monkeypatch.chdir(temp_repo)
        arch_file = temp_repo / "ARCHITECTURE.md"
        arch_file.write_text("# Architecture\nModular monolith\n")
        rules_file = temp_repo / "RULES.md"
        rules_file.write_text("# Rules\nNo spaghetti\n")

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_arch_path": str(arch_file),
            "local_rules_path": str(rules_file),
        }, default_flow_style=False))

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["show", "arch"])

        assert result.exit_code == 0, result.output
        assert "Modular monolith" in result.output
        assert "spaghetti" not in result.output

    def test_show_state_requires_remote_project(self, temp_repo, home_dir, monkeypatch):
        """show state in local-only mode tells user to connect a remote project."""
        monkeypatch.chdir(temp_repo)

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_arch_path": str(temp_repo),
            "local_rules_path": str(temp_repo),
        }, default_flow_style=False))

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["show", "state"])

        assert result.exit_code == 0, result.output
        assert "remote project" in result.output.lower()
        assert "gjalla connect" in result.output.lower()

    def test_show_rules_falls_through_when_only_arch_set(self, temp_repo, home_dir, monkeypatch):
        """show rules with only local_arch_path (no rules path) shows friendly message."""
        monkeypatch.chdir(temp_repo)
        arch_file = temp_repo / "arch.md"
        arch_file.write_text("# Architecture\nModular monolith\n")

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_arch_path": str(arch_file),
        }, default_flow_style=False))

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["show", "rules"])

        assert result.exit_code == 0, result.output
        assert "no rules docs configured" in result.output.lower()

    def test_show_missing_local_rules_path_errors(self, temp_repo, home_dir, monkeypatch):
        """show rules with a missing local_rules_path exits with error."""
        monkeypatch.chdir(temp_repo)

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_rules_path": "/nonexistent/docs",
        }, default_flow_style=False))

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["show", "rules"])

        assert result.exit_code != 0
        assert "no longer exists" in result.output.lower()

    def test_show_without_local_docs_falls_through_to_cache(self, temp_repo, home_dir, monkeypatch):
        """show rules without local_docs_path uses cache (existing behavior)."""
        monkeypatch.chdir(temp_repo)

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": 42,
            "api_url": "https://gjalla.io",
        }, default_flow_style=False))

        cache_dir = gjalla_dir / "cache"
        cache_dir.mkdir()
        (cache_dir / "rules.md").write_text("# Remote Rules\n\n- Rule from server\n")

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["show", "rules"])

        assert result.exit_code == 0, result.output
        assert "Rule from server" in result.output

    def test_show_empty_directory(self, temp_repo, home_dir, monkeypatch):
        """show rules with empty directory prints no-files message."""
        monkeypatch.chdir(temp_repo)
        empty_dir = temp_repo / "empty_docs"
        empty_dir.mkdir()

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_rules_path": str(empty_dir),
        }, default_flow_style=False))

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["show", "rules"])

        assert result.exit_code == 0, result.output
        assert "no files" in result.output.lower()

    def test_show_directory_skips_hidden_files(self, temp_repo, home_dir, monkeypatch):
        """show rules with directory skips dotfiles."""
        monkeypatch.chdir(temp_repo)
        docs_dir = temp_repo / "docs"
        docs_dir.mkdir()
        (docs_dir / ".hidden").write_text("secret\n")
        (docs_dir / "rules.md").write_text("# Visible Rules\n")

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_rules_path": str(docs_dir),
        }, default_flow_style=False))

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["show", "rules"])

        assert result.exit_code == 0, result.output
        assert "Visible Rules" in result.output
        assert "secret" not in result.output

    def test_backward_compat_legacy_local_docs_path(self, temp_repo, home_dir, monkeypatch):
        """Config with old local_docs_path still works for both show arch and show rules."""
        monkeypatch.chdir(temp_repo)
        docs_file = temp_repo / "legacy-docs.md"
        docs_file.write_text("# Legacy Docs\nOld format content\n")

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_docs_path": str(docs_file),
        }, default_flow_style=False))

        runner = CliRunner()

        result_rules = runner.invoke(cli_module.main, ["show", "rules"])
        assert result_rules.exit_code == 0, result_rules.output
        assert "Legacy Docs" in result_rules.output

        result_arch = runner.invoke(cli_module.main, ["show", "arch"])
        assert result_arch.exit_code == 0, result_arch.output
        assert "Legacy Docs" in result_arch.output

    def test_show_no_paths_configured_friendly_message(self, temp_repo, home_dir, monkeypatch):
        """show commands print friendly message when local-only with no paths configured."""
        monkeypatch.chdir(temp_repo)

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "api_url": "https://gjalla.io",
        }, default_flow_style=False))

        runner = CliRunner()

        for subcmd, kind in [("rules", "rules"), ("arch", "architecture")]:
            result = runner.invoke(cli_module.main, ["show", subcmd])
            assert result.exit_code == 0, f"show {subcmd}: {result.output}"
            assert f"no {kind} docs configured" in result.output.lower(), f"show {subcmd}: {result.output}"
            assert "gjalla setup" in result.output.lower(), f"show {subcmd}: {result.output}"

        # state requires remote project, not local docs
        result = runner.invoke(cli_module.main, ["show", "state"])
        assert result.exit_code == 0, f"show state: {result.output}"
        assert "remote project" in result.output.lower(), f"show state: {result.output}"


# ---------------------------------------------------------------------------
# Sync: local-only mode
# ---------------------------------------------------------------------------

class TestSyncLocalOnly:
    """Tests for gjalla sync in local-only mode."""

    def test_sync_local_only_valid_path(self, temp_repo, home_dir, monkeypatch):
        """Sync with local_arch_path/local_rules_path and no project_id returns early."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)

        arch_dir = temp_repo / "arch-docs"
        arch_dir.mkdir()
        (arch_dir / "arch.md").write_text("# Arch\n")

        rules_dir = temp_repo / "rules-docs"
        rules_dir.mkdir()
        (rules_dir / "rules.md").write_text("# Rules\n")

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_arch_path": str(arch_dir),
            "local_rules_path": str(rules_dir),
            "api_url": "https://gjalla.io",
        }, default_flow_style=False))

        runner = CliRunner()
        with patch.object(sync_module, "_upload_attestations") as mock_upload, \
             patch.object(sync_module, "_refresh_context_cache") as mock_refresh:
            result = runner.invoke(cli_module.main, ["sync"])

        assert result.exit_code == 0, result.output
        assert "local-only" in result.output.lower()
        assert "gjalla connect" in result.output.lower()
        mock_upload.assert_not_called()
        mock_refresh.assert_not_called()

    def test_sync_local_only_single_path(self, temp_repo, home_dir, monkeypatch):
        """Sync with only local_arch_path (no rules) still detects local-only mode."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)

        arch_dir = temp_repo / "arch-docs"
        arch_dir.mkdir()
        (arch_dir / "arch.md").write_text("# Arch\n")

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_arch_path": str(arch_dir),
            "api_url": "https://gjalla.io",
        }, default_flow_style=False))

        runner = CliRunner()
        with patch.object(sync_module, "_upload_attestations") as mock_upload, \
             patch.object(sync_module, "_refresh_context_cache") as mock_refresh:
            result = runner.invoke(cli_module.main, ["sync"])

        assert result.exit_code == 0, result.output
        assert "local-only" in result.output.lower()
        mock_upload.assert_not_called()
        mock_refresh.assert_not_called()

    def test_sync_local_only_invalid_path(self, temp_repo, home_dir, monkeypatch):
        """Sync with missing local paths warns but doesn't crash."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": None,
            "local_arch_path": "/nonexistent/arch",
            "api_url": "https://gjalla.io",
        }, default_flow_style=False))

        runner = CliRunner()
        result = runner.invoke(cli_module.main, ["sync"])

        assert result.exit_code == 0, result.output
        assert "no longer exists" in result.output.lower()

    def test_sync_with_project_id_ignores_local_docs(self, temp_repo, home_dir, monkeypatch):
        """Sync with project_id set uses remote flow even if local paths are set."""
        monkeypatch.chdir(temp_repo)
        monkeypatch.delenv("GJALLA_API_KEY", raising=False)

        gjalla_dir = temp_repo / ".gjalla"
        gjalla_dir.mkdir(parents=True, exist_ok=True)
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": 42,
            "local_arch_path": str(temp_repo),
            "local_rules_path": str(temp_repo),
            "api_url": "https://gjalla.io",
        }, default_flow_style=False))

        # Global config with API key
        config_dir = home_dir / ".gjalla"
        config_dir.mkdir(parents=True, exist_ok=True)
        (config_dir / "config.yaml").write_text("api_key: gj_test_key\n")

        runner = CliRunner()
        with patch.object(sync_module, "_upload_attestations") as mock_upload, \
             patch.object(sync_module, "_refresh_context_cache"):
            result = runner.invoke(cli_module.main, ["sync"])

        assert result.exit_code == 0, result.output
        # Should have gone to remote flow and called upload
        mock_upload.assert_called_once()


# ---------------------------------------------------------------------------
# Telemetry: privacy message
# ---------------------------------------------------------------------------

class TestTelemetryPrivacyMessage:
    """Test that privacy message is shown before consent prompt."""

    def test_privacy_message_before_consent(self, home_dir, monkeypatch):
        """check_consent shows privacy message before prompting."""
        monkeypatch.delenv("GJALLA_TELEMETRY", raising=False)
        config_dir = home_dir / ".gjalla"
        config_dir.mkdir(parents=True, exist_ok=True)
        (config_dir / "config.yaml").write_text("api_key: test\n")

        info_messages = []

        with patch("gjalla_precommit.telemetry.sys") as mock_sys, \
             patch("gjalla_precommit.telemetry.Confirm") as mock_confirm, \
             patch("gjalla_precommit.display.output.info") as mock_info:
            mock_sys.stdin.isatty.return_value = True
            mock_confirm.ask.return_value = False
            mock_info.side_effect = lambda msg: info_messages.append(msg)
            telemetry_module.check_consent()

        assert any("does not use an LLM" in msg for msg in info_messages)
