"""Integration tests for error handling and edge cases.

These tests verify that the patcher handles various error scenarios
gracefully, including corrupt files, permission issues, and incomplete
structures.
"""

from __future__ import annotations

import json
import os
import stat
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from electron_angle_patcher import ElectronAnglePatcher
from electron_angle_patcher.executable_patcher import ExecutableWrapper
from electron_angle_patcher.models import PatchResult
from electron_angle_patcher.scanner import LocalStateScanner
from electron_angle_patcher.settings_patcher import ChromiumSettingsPatcher
from tests.fixtures.app_builders import (
    create_brave_app_bundle,
    create_local_state_file,
)


class TestPermissionErrors:
    """Tests for permission-related error scenarios."""

    def test_readonly_app_permission_denied(
        self,
        integration_temp_dir: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test that read-only directory returns permission denied."""
        # Create app bundle
        app_path = create_brave_app_bundle(integration_temp_dir, "fresh")

        # Mock can_write_to_path to return False
        monkeypatch.setattr(
            "electron_angle_patcher.executable_patcher.can_write_to_path",
            lambda path: False,
        )

        # Try to patch
        wrapper = ExecutableWrapper(app_path, "Brave Browser")
        result = wrapper.patch()

        # Verify permission denied
        assert result == PatchResult.PERMISSION_DENIED

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

    def test_readonly_file_handled(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that read-only file is handled gracefully."""
        # Create state file
        state_file = integration_temp_dir / "Local State"
        state_file.write_text('{"browser": {"enabled_labs_experiments": []}}')

        # Make file read-only
        state_file.chmod(0o444)

        try:
            # Try to patch (should fail)
            patcher = ChromiumSettingsPatcher(state_file)
            result = patcher.patch()

            # Should return error (can't write)
            assert result == PatchResult.ERROR
        finally:
            # Restore permissions for cleanup
            state_file.chmod(0o644)


class TestCorruptFiles:
    """Tests for corrupt or malformed file scenarios."""

    def test_corrupt_local_state_recovered(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that corrupt Local State is replaced with valid structure."""
        # Create corrupt file
        state_file = integration_temp_dir / "Local State"
        state_file.write_text("{invalid json content}")

        # Patch should recover
        patcher = ChromiumSettingsPatcher(state_file)
        result = patcher.patch()

        # Verify success
        assert result == PatchResult.SUCCESS

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

    def test_partial_json_recovered(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that truncated JSON is handled."""
        # Create truncated JSON file
        state_file = integration_temp_dir / "Local State"
        state_file.write_text('{"browser": {"enabled_labs_experiments": ["use-angle')

        # Patch should handle error
        patcher = ChromiumSettingsPatcher(state_file)
        result = patcher.patch()

        # Verify success (creates new file)
        assert result == PatchResult.SUCCESS

    def test_empty_local_state_file(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that completely empty file is handled."""
        # Create empty file
        state_file = integration_temp_dir / "Local State"
        state_file.write_text("")

        # Patch should create valid structure
        patcher = ChromiumSettingsPatcher(state_file)
        result = patcher.patch()

        # Verify success
        assert result == PatchResult.SUCCESS

        # Verify valid structure created
        data = json.loads(state_file.read_text())
        assert "browser" in data


class TestIncompleteStructures:
    """Tests for incomplete or malformed app bundle structures."""

    def test_app_not_found(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that non-existent app returns NOT_FOUND."""
        # Try to patch non-existent app
        app_path = integration_temp_dir / "NonExistent.app"
        wrapper = ExecutableWrapper(app_path, "NonExistent")
        result = wrapper.patch()

        # Verify not found
        assert result == PatchResult.NOT_FOUND

    def test_partial_structure_missing_executable(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that app bundle without executable returns NOT_FOUND."""
        # Create app bundle without executable
        app_path = integration_temp_dir / "Incomplete.app"
        macos_path = app_path / "Contents" / "MacOS"
        macos_path.mkdir(parents=True)
        # Don't create executable

        # Try to patch
        wrapper = ExecutableWrapper(app_path, "Incomplete")
        result = wrapper.patch()

        # Verify not found
        assert result == PatchResult.NOT_FOUND

    def test_partial_structure_missing_macos_dir(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that app bundle without MacOS directory returns NOT_FOUND."""
        # Create app bundle without MacOS directory
        app_path = integration_temp_dir / "Incomplete.app"
        contents_path = app_path / "Contents"
        contents_path.mkdir(parents=True)
        # Don't create MacOS directory

        # Try to patch
        wrapper = ExecutableWrapper(app_path, "Incomplete")
        result = wrapper.patch()

        # Verify not found
        assert result == PatchResult.NOT_FOUND

    def test_local_state_missing_browser_section(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that Local State without browser section is handled."""
        # Create file without browser section
        state_file = integration_temp_dir / "Local State"
        state_file.write_text('{"some_other_key": "value"}')

        # Patch should add browser section
        patcher = ChromiumSettingsPatcher(state_file)
        result = patcher.patch()

        # Verify success
        assert result == PatchResult.SUCCESS

        # Verify browser section added
        data = json.loads(state_file.read_text())
        assert "browser" in data
        assert "some_other_key" in data  # Original data preserved


class TestEdgeCases:
    """Tests for edge cases and unusual scenarios."""

    def test_empty_directory(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that scanning empty directory returns empty list."""
        # Create empty directory
        empty_dir = integration_temp_dir / "Empty"
        empty_dir.mkdir()

        # Scan
        scanner = LocalStateScanner()
        results = scanner.scan(empty_dir)

        # Verify empty results
        assert len(results) == 0

    def test_directory_with_only_hidden_files(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that directory with only hidden files returns empty list."""
        # Create directory with hidden subdirectory
        test_dir = integration_temp_dir / "Test"
        test_dir.mkdir()
        hidden_dir = test_dir / ".hidden"
        hidden_dir.mkdir()
        (hidden_dir / "Local State").write_text('{"browser": {}}')

        # Scan
        scanner = LocalStateScanner()
        results = scanner.scan(test_dir)

        # Verify hidden skipped
        assert len(results) == 0

    def test_symlinks_not_followed(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that scanner handles symlinks appropriately."""
        # This test verifies symlinks don't cause crashes
        # Actual behavior depends on os.walk implementation

        # Create directory with symlink (skip if not supported)
        try:
            target_dir = integration_temp_dir / "Target"
            target_dir.mkdir()
            link_dir = integration_temp_dir / "Link"

            # Try to create symlink (may fail on some systems)
            try:
                link_dir.symlink_to(target_dir)
            except OSError:
                pytest.skip("Symlinks not supported on this system")

            # Scan should handle without error
            scanner = LocalStateScanner()
            results = scanner.scan(integration_temp_dir)

            # Should complete without crash
            assert isinstance(results, list)

        except Exception:
            pytest.skip("Symlink test not applicable")

    def test_very_long_path(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that very long paths are handled."""
        # Create deeply nested structure
        deep_dir = integration_temp_dir
        for i in range(10):
            deep_dir = deep_dir / f"very_long_directory_name_{i}"

        state_file = create_local_state_file(deep_dir, "DeepApp", "empty")

        # Patch should work with long path
        patcher = ChromiumSettingsPatcher(state_file)
        result = patcher.patch()

        # Verify success
        assert result == PatchResult.SUCCESS

    def test_special_characters_in_app_name(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that special characters in app names are handled."""
        # Create app with special characters
        support_dir = integration_temp_dir / "Application Support"
        app_name = "Test App (Special) [2024]"
        app_dir = support_dir / app_name
        app_dir.mkdir(parents=True)

        state_file = app_dir / "Local State"
        state_file.write_text('{"browser": {"enabled_labs_experiments": []}}')

        # Patch should work
        patcher = ChromiumSettingsPatcher(state_file)
        result = patcher.patch()

        # Verify success
        assert result == PatchResult.SUCCESS

    def test_unicode_in_path(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that Unicode characters in paths are handled."""
        # Create directory with Unicode characters
        support_dir = integration_temp_dir / "Application Support"
        app_name = "ТестовоеПриложение"  # Russian characters
        app_dir = support_dir / app_name

        try:
            app_dir.mkdir(parents=True)
        except OSError:
            pytest.skip("Unicode directory names not supported")

        state_file = app_dir / "Local State"
        state_file.write_text('{"browser": {"enabled_labs_experiments": []}}')

        # Patch should work
        patcher = ChromiumSettingsPatcher(state_file)
        result = patcher.patch()

        # Verify success
        assert result == PatchResult.SUCCESS


class TestPartialFailureScenarios:
    """Tests for scenarios with mixed success/failure."""

    def test_partial_failure_scenario(
        self,
        integration_temp_dir: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Test scenario where some operations succeed and others fail."""
        # Create multiple state files
        support_dir = integration_temp_dir / "Application Support"
        state1 = create_local_state_file(support_dir, "App1", "empty")
        state2 = create_local_state_file(support_dir, "App2", "empty")
        state3 = create_local_state_file(support_dir, "App3", "empty")

        # Make state2 read-only (will fail)
        state2.chmod(0o444)

        results = []
        for state_file in [state1, state2, state3]:
            patcher = ChromiumSettingsPatcher(state_file)
            result = patcher.patch()
            results.append((state_file.name, result))

        try:
            # Verify mixed results
            assert results[0][1] == PatchResult.SUCCESS  # App1
            assert results[1][1] == PatchResult.ERROR  # App2 (read-only)
            assert results[2][1] == PatchResult.SUCCESS  # App3
        finally:
            # Restore permissions for cleanup
            state2.chmod(0o644)

    def test_scanner_with_some_invalid_paths(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test scanner when some paths are invalid."""
        # Create valid app
        support_dir = integration_temp_dir / "Application Support"
        create_local_state_file(support_dir, "ValidApp", "empty")

        # Create invalid structure (not an app)
        invalid_dir = support_dir / "NotAnApp"
        invalid_dir.mkdir()
        (invalid_dir / "some_file.txt").write_text("not a Local State file")

        # Scan should handle gracefully
        scanner = LocalStateScanner()
        results = scanner.scan(support_dir)

        # Should find only valid app
        assert len(results) == 1
        assert results[0].name == "ValidApp"

    def test_patch_already_patched_then_corrupt(
        self,
        integration_temp_dir: Path,
        mock_codesign_success: MagicMock,
        mock_xattr_success: MagicMock,
    ) -> None:
        """Test patching app that becomes corrupt after first patch."""
        # Create fresh app
        app_path = create_brave_app_bundle(integration_temp_dir, "fresh")

        # First patch - should succeed
        wrapper = ExecutableWrapper(app_path, "Brave Browser")
        result1 = wrapper.patch()
        assert result1 == PatchResult.SUCCESS

        # Corrupt the wrapper (simulate error state)
        executable = app_path / "Contents" / "MacOS" / "Brave Browser"
        executable.write_text("corrupt content")

        # Second patch - should handle gracefully
        # Since backup exists, it will detect as already patched
        result2 = wrapper.patch()
        # May return ALREADY_PATCHED or ERROR depending on wrapper validation
        assert result2 in [PatchResult.ALREADY_PATCHED, PatchResult.ERROR]


class TestResourceCleanup:
    """Tests for proper resource handling and cleanup."""

    def test_file_handles_closed(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that file handles are properly closed."""
        # Create and patch multiple files
        support_dir = integration_temp_dir / "Application Support"

        for i in range(10):
            state_file = create_local_state_file(
                support_dir, f"App{i}", "empty"
            )
            patcher = ChromiumSettingsPatcher(state_file)
            patcher.patch()

        # On Unix-like systems, we should be able to delete the directory
        # if all file handles are closed
        # This test verifies no resource leaks
        import shutil
        try:
            shutil.rmtree(support_dir)
            # If we get here, all handles were closed
            assert True
        except OSError:
            # If deletion fails, it might indicate a handle leak
            # (but could also be other issues)
            pytest.fail("Directory cleanup failed - possible resource leak")


class TestConcurrentAccess:
    """Tests for concurrent access scenarios."""

    def test_patch_same_file_twice(
        self,
        integration_temp_dir: Path,
    ) -> None:
        """Test that patching the same file twice is safe."""
        # Create state file
        state_file = create_local_state_file(
            integration_temp_dir, "TestApp", "with_angle"
        )

        # First patch
        patcher1 = ChromiumSettingsPatcher(state_file)
        result1 = patcher1.patch()
        assert result1 == PatchResult.SUCCESS

        # Second patch (idempotent)
        patcher2 = ChromiumSettingsPatcher(state_file)
        result2 = patcher2.patch()
        assert result2 == PatchResult.SUCCESS

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