"""Unit tests for patcher classes."""

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

from electron_angle_patcher import (
    AngleVariant,
    AppInfo,
    ChromiumSettingsPatcher,
    ExecutableWrapper,
    LghubPatcher,
    PatchResult,
    PatchSummary,
)


class TestAppInfo:
    """Tests for AppInfo dataclass."""

    def test_creation(self) -> None:
        """Test AppInfo creation."""
        app = AppInfo(name="TestApp", state_file=Path("/path/to/file"))
        assert app.name == "TestApp"
        assert app.state_file == Path("/path/to/file")
        assert app.already_patched is False

    def test_with_patched_flag(self) -> None:
        """Test AppInfo with already_patched flag."""
        app = AppInfo(name="TestApp", state_file=Path("/path"), already_patched=True)
        assert app.already_patched is True


class TestPatchSummary:
    """Tests for PatchSummary dataclass."""

    def test_empty_summary(self) -> None:
        """Test empty summary."""
        summary = PatchSummary()
        assert summary.total == 0
        assert len(summary.successful) == 0
        assert len(summary.failed) == 0

    def test_total_count(self) -> None:
        """Test total count calculation."""
        summary = PatchSummary(
            successful=["app1", "app2"],
            already_patched=["app3"],
            not_found=["app4"],
            failed=["app5"],
        )
        assert summary.total == 5


class TestAngleVariant:
    """Tests for AngleVariant enum."""

    def test_values(self) -> None:
        """Test enum values."""
        assert AngleVariant.DEFAULT.value == "0"
        assert AngleVariant.OPENGL.value == "1"
        assert AngleVariant.METAL.value == "2"


class TestChromiumSettingsPatcher:
    """Tests for ChromiumSettingsPatcher class."""

    def test_init(self) -> None:
        """Test patcher initialization."""
        patcher = ChromiumSettingsPatcher(Path("/tmp/Local State"))
        assert patcher._desired_key == "use-angle"
        assert patcher._desired_value == AngleVariant.OPENGL.value

    def test_read_state_file_creates_new(self, temp_dir: Path) -> None:
        """Test reading creates new file if missing."""
        state_path = temp_dir / "new_local_state"
        patcher = ChromiumSettingsPatcher(state_path)
        data = patcher._read_state_file()
        assert data == {}
        assert state_path.parent.exists()

    def test_read_state_file_parses_existing(self, temp_dir: Path) -> None:
        """Test reading parses existing file."""
        state_path = temp_dir / "Local State"
        test_data = {"browser": {"test": "value"}}
        state_path.write_text(json.dumps(test_data))

        patcher = ChromiumSettingsPatcher(state_path)
        data = patcher._read_state_file()
        assert data == test_data

    def test_read_state_file_handles_corrupt(self, temp_dir: Path) -> None:
        """Test reading handles corrupt JSON."""
        state_path = temp_dir / "Local State"
        state_path.write_text("invalid json")

        patcher = ChromiumSettingsPatcher(state_path)
        data = patcher._read_state_file()
        assert data == {}

    def test_ensure_structure(self) -> None:
        """Test structure is ensured."""
        patcher = ChromiumSettingsPatcher(Path("/tmp/test"))
        data = {}
        patcher._ensure_structure(data)
        assert "browser" in data
        assert "enabled_labs_experiments" in data["browser"]

    def test_find_and_update_entry_existing(self) -> None:
        """Test updating existing use-angle entry."""
        patcher = ChromiumSettingsPatcher(Path("/tmp/test"))
        experiments = ["use-angle@0", "other@1"]
        result = patcher._find_and_update_entry(experiments)
        assert result is True
        assert experiments[0] == "use-angle@1"

    def test_find_and_update_entry_correct_value(self) -> None:
        """Test when use-angle already has correct value."""
        patcher = ChromiumSettingsPatcher(Path("/tmp/test"))
        experiments = ["use-angle@1", "other@1"]
        result = patcher._find_and_update_entry(experiments)
        assert result is True

    def test_find_and_update_entry_not_found(self) -> None:
        """Test when use-angle entry doesn't exist."""
        patcher = ChromiumSettingsPatcher(Path("/tmp/test"))
        experiments = ["other@1", "another@2"]
        result = patcher._find_and_update_entry(experiments)
        assert result is False

    def test_add_entry_if_missing(self) -> None:
        """Test adding entry when missing."""
        patcher = ChromiumSettingsPatcher(Path("/tmp/test"))
        experiments = ["other@1"]
        result = patcher._add_entry_if_missing(experiments)
        assert result is True
        assert "use-angle@1" in experiments

    def test_add_entry_if_exists(self) -> None:
        """Test when entry already exists."""
        patcher = ChromiumSettingsPatcher(Path("/tmp/test"))
        experiments = ["use-angle@1"]
        result = patcher._add_entry_if_missing(experiments)
        assert result is False
        assert len(experiments) == 1

    def test_write_state_file(self, temp_dir: Path) -> None:
        """Test writing state file."""
        state_path = temp_dir / "Local State"
        patcher = ChromiumSettingsPatcher(state_path)
        data = {"browser": {"enabled_labs_experiments": []}}
        result = patcher._write_state_file(data)
        assert result is True
        assert state_path.exists()

    def test_patch_success(self, temp_dir: Path) -> None:
        """Test successful patch operation."""
        state_path = temp_dir / "Local State"
        state_path.write_text('{"browser": {"enabled_labs_experiments": []}}')

        patcher = ChromiumSettingsPatcher(state_path)
        result = patcher.patch()
        assert result == PatchResult.SUCCESS

    def test_patch_adds_to_empty(self, temp_dir: Path) -> None:
        """Test patch adds entry to empty list."""
        state_path = temp_dir / "Local State"
        state_path.write_text('{"browser": {"enabled_labs_experiments": []}}')

        patcher = ChromiumSettingsPatcher(state_path)
        patcher.patch()

        updated = json.loads(state_path.read_text())
        assert "use-angle@1" in updated["browser"]["enabled_labs_experiments"]


class TestExecutableWrapper:
    """Tests for ExecutableWrapper class."""

    def test_init(self, temp_dir: Path) -> None:
        """Test wrapper initialization."""
        app_path = temp_dir / "TestApp.app"
        macos_path = app_path / "Contents" / "MacOS"
        macos_path.mkdir(parents=True)
        (macos_path / "TestApp").write_text("fake executable")

        wrapper = ExecutableWrapper(app_path, "TestApp")
        assert wrapper._app_path == app_path
        assert wrapper._executable_name == "TestApp"
        assert wrapper._executable_path == macos_path / "TestApp"

    @patch("electron_angle_patcher.executable_patcher.can_write_to_path", return_value=False)
    def test_patch_no_permission(self, mock_write: MagicMock, temp_dir: Path) -> None:
        """Test patch fails without write permission."""
        app_path = temp_dir / "TestApp.app"
        macos_path = app_path / "Contents" / "MacOS"
        macos_path.mkdir(parents=True)
        (macos_path / "TestApp").write_text("fake executable")

        wrapper = ExecutableWrapper(app_path, "TestApp")
        result = wrapper.patch()
        assert result == PatchResult.PERMISSION_DENIED

    def test_patch_app_not_found(self, temp_dir: Path) -> None:
        """Test patch when app doesn't exist."""
        wrapper = ExecutableWrapper(temp_dir / "Missing.app", "TestApp")
        result = wrapper.patch()
        assert result == PatchResult.NOT_FOUND

    def test_already_wrapped(self, temp_dir: Path) -> None:
        """Test detecting already wrapped executable."""
        app_path = temp_dir / "TestApp.app"
        macos_path = app_path / "Contents" / "MacOS"
        macos_path.mkdir(parents=True)

        # Create backup
        original = macos_path / "TestApp.original"
        original.write_text("original")
        (macos_path / "TestApp").write_text("#!/bin/bash\nwrapper")

        wrapper = ExecutableWrapper(app_path, "TestApp")
        result = wrapper.patch()
        assert result == PatchResult.ALREADY_PATCHED


class TestLghubPatcher:
    """Tests for LghubPatcher class."""

    def test_init_default_path(self) -> None:
        """Test initialization with default path."""
        patcher = LghubPatcher()
        # Just check it doesn't crash
        assert patcher._app_path is not None

    def test_init_custom_path(self, temp_dir: Path) -> None:
        """Test initialization with custom path."""
        patcher = LghubPatcher(temp_dir / "lghub.app")
        assert patcher._app_path == temp_dir / "lghub.app"

    def test_patch_app_not_found(self, temp_dir: Path) -> None:
        """Test patch when app doesn't exist."""
        patcher = LghubPatcher(temp_dir / "Missing.app")
        result = patcher.patch()
        assert result == PatchResult.NOT_FOUND
