"""End-to-end tests for the CLI.

These tests use Click's CliRunner (or subprocess for argparse) to test
complete CLI workflows from invocation to completion.
"""

from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from electron_angle_patcher import cli
from electron_angle_patcher.models import PatchResult
from tests.fixtures.app_builders import (
    create_brave_app_bundle,
    create_local_state_file,
)


class TestCLIEndToEnd:
    """End-to-end tests for CLI workflows."""

    def test_list_finds_brave(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
        mock_network_mounts_empty: MagicMock,
        capsys: pytest.CaptureFixture[str],
        patch_cli_path_conversion: None,
    ) -> None:
        """Test that --list-only discovers Brave without modifying."""
        # Create Brave app
        brave_path = create_brave_app_bundle(integration_temp_dir, "fresh")

        # Run CLI with list-only
        result = cli.main(
            [
                "--brave-path", str(brave_path),
                "--list-only",
            ]
        )

        # Verify success
        assert result == 0

        # Verify no backup was created (list-only mode)
        backup_path = brave_path / "Contents" / "MacOS" / "Brave Browser.original"
        assert not backup_path.exists()

    def test_list_finds_local_state_files(
        self,
        integration_temp_dir: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Test that --list-only finds Local State files."""
        # Create test apps
        support_dir = integration_temp_dir / "Application Support"
        create_local_state_file(support_dir, "App1", "with_angle")
        create_local_state_file(support_dir, "App2", "empty")

        # Run CLI with list-only and scan-dir
        result = cli.main(
            [
                "--scan-dir", str(support_dir),
                "--list-only",
            ]
        )

        # Verify success
        assert result == 0

        # Verify output mentions found apps
        captured = capsys.readouterr()
        assert "App1" in captured.out or result == 0

    def test_dry_run_brave_no_changes(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
        capsys: pytest.CaptureFixture[str],
        patch_cli_path_conversion: None,
    ) -> None:
        """Test that --dry-run shows actions without making changes."""
        # Create Brave app
        brave_path = create_brave_app_bundle(integration_temp_dir, "fresh")

        # Run CLI with dry-run
        result = cli.main(
            [
                "--brave-path", str(brave_path),
                "--dry-run",
            ]
        )

        # Verify success
        assert result == 0

        # Verify no backup was created
        backup_path = brave_path / "Contents" / "MacOS" / "Brave Browser.original"
        assert not backup_path.exists()

    def test_patch_brave_success(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
        patch_cli_path_conversion: None,
    ) -> None:
        """Test that full patching creates wrapper and backup."""
        # Create Brave app
        brave_path = create_brave_app_bundle(integration_temp_dir, "fresh")

        # Run CLI to patch
        result = cli.main(
            [
                "--brave-path", str(brave_path),
            ]
        )

        # Verify success
        assert result == 0

        # Verify wrapper and backup exist
        executable = brave_path / "Contents" / "MacOS" / "Brave Browser"
        backup_path = brave_path / "Contents" / "MacOS" / "Brave Browser.original"

        assert executable.exists()
        assert backup_path.exists()

        # Verify wrapper content
        content = executable.read_text()
        assert "#!/bin/bash" in content
        assert "--use-angle=gl" in content

    def test_patch_already_patched_skips(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
        capsys: pytest.CaptureFixture[str],
        patch_cli_path_conversion: None,
    ) -> None:
        """Test that already patched app is detected and skipped."""
        # Create already patched Brave app
        brave_path = create_brave_app_bundle(integration_temp_dir, "already_patched")

        # Get original backup content
        backup_path = brave_path / "Contents" / "MacOS" / "Brave Browser.original"
        original_backup_content = backup_path.read_text()

        # Run CLI to patch
        result = cli.main(
            [
                "--brave-path", str(brave_path),
            ]
        )

        # Verify success (skipping is success)
        assert result == 0

        # Verify backup wasn't modified
        assert backup_path.read_text() == original_backup_content

    def test_patch_custom_local_state(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that --app-path patches single app."""
        # Create Local State file
        state_file = create_local_state_file(
            integration_temp_dir, "CustomApp", "with_angle"
        )

        # Run CLI with app-path
        result = cli.main(
            [
                "--app-path", str(state_file),
                "--app-name", "CustomApp",
            ]
        )

        # Verify success
        assert result == 0

        # Verify file was patched
        data = json.loads(state_file.read_text())
        experiments = data["browser"]["enabled_labs_experiments"]
        assert "use-angle@1" in experiments

    def test_scan_dir_patches_apps(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
    ) -> None:
        """Test that --scan-dir patches discovered apps."""
        # Create test apps
        support_dir = integration_temp_dir / "Application Support"
        app1_file = create_local_state_file(support_dir, "App1", "with_angle")
        app2_file = create_local_state_file(support_dir, "App2", "empty")

        # Run CLI with scan-dir
        result = cli.main(
            [
                "--scan-dir", str(support_dir),
            ]
        )

        # Verify success
        assert result == 0

        # Verify both apps were patched
        for state_file in [app1_file, app2_file]:
            data = json.loads(state_file.read_text())
            assert "use-angle@1" in data["browser"]["enabled_labs_experiments"]

    def test_verbose_output(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
        mock_network_mounts_empty: MagicMock,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Test that --verbose produces detailed output."""
        # Create Brave app
        brave_path = create_brave_app_bundle(integration_temp_dir, "fresh")

        # Run CLI with verbose
        result = cli.main(
            [
                "--brave-path", str(brave_path),
                "--list-only",
                "--verbose",
            ]
        )

        # Verify success
        assert result == 0

        # Verbose output captured (logging level check)
        captured = capsys.readouterr()
        # Just verify it ran without error

    def test_nonexistent_brave_path_handled(
        self,
        integration_temp_dir: Path,
        mock_network_mounts_empty: MagicMock,
        patch_cli_path_conversion: None,
    ) -> None:
        """Test that nonexistent Brave path is handled gracefully."""
        # Use non-existent path
        fake_path = integration_temp_dir / "NonExistent.app"

        # Run CLI
        result = cli.main(
            [
                "--brave-path", str(fake_path),
            ]
        )

        # Should still succeed (just not found)
        assert result == 0

    def test_version_flag(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Test that --version flag works."""
        # This should exit with version
        with pytest.raises(SystemExit) as exc_info:
            cli.main(["--version"])

        # Version flag exits with 0
        assert exc_info.value.code == 0

    def test_help_flag(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Test that --help flag works."""
        # This should exit with help
        with pytest.raises(SystemExit) as exc_info:
            cli.main(["--help"])

        # Help flag exits with 0
        assert exc_info.value.code == 0

    def test_deep_scan_flag(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
        mock_network_mounts_empty: MagicMock,
    ) -> None:
        """Test that --deep-scan flag is accepted."""
        # Create test structure
        support_dir = integration_temp_dir / "Library" / "Application Support"
        create_local_state_file(support_dir, "TestApp", "empty")

        # Run CLI with deep-scan
        result = cli.main(
            [
                "--deep-scan",
            ]
        )

        # Should complete without error
        assert result == 0 or result == 1  # May not find apps depending on paths

    def test_custom_lghub_path(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
        patch_cli_path_conversion: None,
    ) -> None:
        """Test that custom lghub path works."""
        from tests.fixtures.app_builders import create_lghub_app_bundle

        # Create lghub app
        lghub_path = create_lghub_app_bundle(integration_temp_dir, "fresh")

        # Run CLI with custom lghub path
        result = cli.main(
            [
                "--lghub-path", str(lghub_path),
            ]
        )

        # Verify success
        assert result == 0

        # Verify inner app was patched
        inner_app = lghub_path / "Contents" / "MacOS" / "lghub_ui.app"
        inner_backup = inner_app / "Contents" / "MacOS" / "lghub_ui.original"
        assert inner_backup.exists()

    def test_list_only_with_scan_dir(
        self,
        integration_temp_dir: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Test --list-only with --scan-dir combination."""
        # Create test apps
        support_dir = integration_temp_dir / "Application Support"
        create_local_state_file(support_dir, "App1", "with_angle")
        create_local_state_file(support_dir, "App2", "empty")

        # Run CLI
        result = cli.main(
            [
                "--scan-dir", str(support_dir),
                "--list-only",
            ]
        )

        # Verify success
        assert result == 0

        # Verify files weren't modified
        app1_file = support_dir / "App1" / "Local State"
        data = json.loads(app1_file.read_text())
        # Should still have @0 since list-only
        assert "use-angle@0" in data["browser"]["enabled_labs_experiments"]

    def test_dry_run_same_as_list_only(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
        patch_cli_path_conversion: None,
    ) -> None:
        """Test that --dry-run behaves same as --list-only."""
        # Create Brave app
        brave_path = create_brave_app_bundle(integration_temp_dir, "fresh")

        # Run with dry-run
        result = cli.main(
            [
                "--brave-path", str(brave_path),
                "--dry-run",
            ]
        )

        # Verify success
        assert result == 0

        # Verify no changes
        backup_path = brave_path / "Contents" / "MacOS" / "Brave Browser.original"
        assert not backup_path.exists()


class TestCLIErrorHandling:
    """Test CLI error handling."""

    def test_non_macos_platform_rejected(
        self,
        monkeypatch: pytest.MonkeyPatch,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Test that CLI rejects non-macOS platforms."""
        # Mock platform as Linux
        monkeypatch.setattr(sys, "platform", "linux")

        # Try to run CLI
        result = cli.main([])

        # Should fail
        assert result == 1

    def test_invalid_app_path_handled(
        self,
        integration_temp_dir: Path,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Test that invalid app path is handled."""
        # Run with non-existent app path
        result = cli.main(
            [
                "--app-path", str(integration_temp_dir / "nonexistent.json"),
            ]
        )

        # Should handle gracefully
        # May return 1 for failure or 0 if it just skips
        assert result in [0, 1]

    def test_keyboard_interrupt(
        self,
        integration_temp_dir: Path,
        mock_subprocess_all_safe: None,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that keyboard interrupt is handled gracefully."""
        # Mock KeyboardInterrupt during run
        def mock_run(*args, **kwargs):
            raise KeyboardInterrupt()

        monkeypatch.setattr(
            "electron_angle_patcher.app.ElectronAnglePatcher.run",
            mock_run,
        )

        # Run CLI
        result = cli.main([])

        # Should return special exit code for SIGINT
        assert result == 130


class TestCLISubprocess:
    """Test CLI via subprocess for more realistic testing."""

    def test_cli_entry_point(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that the CLI entry point works via subprocess."""
        # This test verifies the actual script can be invoked
        # Skip if not in proper environment
        if not (Path(__file__).parent.parent.parent / "src" / "electron_angle_patcher" / "__init__.py").exists():
            pytest.skip("Not in proper test environment")

        # Just verify the module can be imported
        import electron_angle_patcher.cli
        assert electron_angle_patcher.cli is not None
