import json
import os
from pathlib import Path
from unittest.mock import Mock

import pytest

import src.presets.storage_system as storage_system


class MockPresetItem:
    """Simple container for preset collection entries."""

    def __init__(self):
        self.name = ""
        self.source = ""


class MockPresetCollection:
    """Minimal stand-in for Blender's CollectionProperty."""

    def __init__(self):
        self._items = []
        self.cleared = False

    def clear(self):
        self.cleared = True
        self._items.clear()

    def add(self):
        item = MockPresetItem()
        self._items.append(item)
        return item
    
    def remove(self, index):
        self._items.pop(index)

    def __iter__(self):
        return iter(self._items)

    def __len__(self):
        return len(self._items)


class MockProps:
    """Mock text texture properties containing preset collection."""

    def __init__(self):
        self.presets = MockPresetCollection()
        self.save_with_blend_file = False
        self.new_preset_name = ""
        self.active_preset_index = 0
        self.preset_search_query = ""
        self.last_loaded_preset = ""


class MockScene(dict):
    """Dictionary-like scene object supporting custom properties."""

    def __init__(self, props=None):
        super().__init__()
        self.text_texture_props = props

    def __bool__(self):
        # Scene should be truthy even when no custom properties are set
        return True


@pytest.fixture
def make_scene(mock_bpy_fixture):
    """Factory for assigning a mock scene to bpy.context."""

    def _make_scene(props=None):
        scene = MockScene(props)
        mock_bpy_fixture.context.scene = scene
        return scene

    return _make_scene


def test_get_persistent_preset_path_creates_directory(tmp_path, mock_bpy_fixture):
    config_dir = tmp_path / "config"
    config_dir.mkdir()
    mock_bpy_fixture.utils.user_resource.return_value = str(config_dir)

    path = storage_system.get_persistent_preset_path()
    expected = config_dir / "text_texture_generator" / "presets"

    assert Path(path) == expected
    assert expected.is_dir()


def test_save_preset_unified_persistent_writes_file(tmp_path, monkeypatch):
    persistent_dir = tmp_path / "persistent"
    persistent_dir.mkdir()
    monkeypatch.setattr(storage_system, "get_persistent_preset_path", lambda: str(persistent_dir))

    preset_data = {"text": "hello", "font_size": 24}
    result = storage_system.save_preset_unified(preset_data, "SamplePreset", save_to_blend_file=False)

    saved_path = persistent_dir / "SamplePreset.json"
    assert result is True
    assert saved_path.exists()
    with saved_path.open("r") as handle:
        assert json.load(handle) == preset_data


def test_save_preset_unified_blend_delegates(monkeypatch):
    mock_save = Mock(return_value=True)
    monkeypatch.setattr(storage_system, "save_blend_file_preset", mock_save)

    payload = {"text": "blend storage"}
    result = storage_system.save_preset_unified(payload, "BlendPreset", save_to_blend_file=True)

    assert result is True
    mock_save.assert_called_once_with("BlendPreset", payload)


def test_get_blend_file_presets_initializes_empty_collection(make_scene):
    scene = make_scene()

    presets = storage_system.get_blend_file_presets()

    assert presets == {}
    assert scene["text_texture_presets"] == "{}"


def test_save_blend_file_preset_stores_json_in_scene(make_scene):
    scene = make_scene()
    preset_content = {"text": "stored", "font_size": 18}

    result = storage_system.save_blend_file_preset("StoredPreset", preset_content)

    assert result is True
    stored = json.loads(scene["text_texture_presets"])
    assert stored["StoredPreset"] == preset_content
    roundtrip = storage_system.get_blend_file_presets()
    assert roundtrip["StoredPreset"] == preset_content


def test_delete_blend_file_preset_removes_entry(make_scene):
    scene = make_scene()
    storage_system.save_blend_file_preset("TempPreset", {"value": 1})

    removed = storage_system.delete_blend_file_preset("TempPreset")
    assert removed is True
    assert json.loads(scene["text_texture_presets"]) == {}

    removed_again = storage_system.delete_blend_file_preset("TempPreset")
    assert removed_again is False


def test_get_unified_preset_list_prioritizes_blend_presets(tmp_path, monkeypatch):
    persistent_dir = tmp_path / "persistent"
    persistent_dir.mkdir()
    monkeypatch.setattr(storage_system, "get_persistent_preset_path", lambda: str(persistent_dir))

    with (persistent_dir / "alpha.json").open("w") as handle:
        json.dump({"source": "persistent"}, handle)
    with (persistent_dir / "shared.json").open("w") as handle:
        json.dump({"source": "persistent-shared"}, handle)

    monkeypatch.setattr(
        storage_system,
        "get_blend_file_presets",
        lambda: {
            "shared": {"source": "blend"},
            "blend_only": {"source": "blend-only"},
        },
    )

    presets = storage_system.get_unified_preset_list()

    assert presets["alpha"][1] == "PERSISTENT_FILE"
    assert presets["alpha"][0]["source"] == "persistent"
    assert presets["shared"][1] == "BLEND_FILE"
    assert presets["shared"][0]["source"] == "blend"
    assert presets["blend_only"][1] == "BLEND_FILE"


def test_migrate_from_legacy_system_migrates_files_and_creates_backup(tmp_path, monkeypatch):
    legacy_dir = tmp_path / "legacy"
    persistent_dir = tmp_path / "persistent"
    legacy_dir.mkdir()
    persistent_dir.mkdir()

    legacy_payload = {"example": True}
    with (legacy_dir / "legacy_preset.json").open("w") as handle:
        json.dump(legacy_payload, handle)
    with (persistent_dir / "existing.json").open("w") as handle:
        json.dump({"existing": True}, handle)

    monkeypatch.setattr(storage_system, "get_preset_path", lambda: str(legacy_dir))
    monkeypatch.setattr(storage_system, "get_persistent_preset_path", lambda: str(persistent_dir))

    result = storage_system.migrate_from_legacy_system()

    migrated_file = persistent_dir / "legacy_preset.json"
    backup_file = persistent_dir / "legacy_backup" / "legacy_preset.json"
    marker_file = persistent_dir / ".migration_complete"

    assert result is True
    assert migrated_file.exists()
    assert json.loads(migrated_file.read_text()) == legacy_payload
    assert backup_file.exists()
    assert marker_file.exists()


def test_initialize_presets_populates_props_collection(make_scene, monkeypatch):
    props = MockProps()
    # Seed with existing entry to ensure clear() is invoked
    props.presets.add().name = "stale"
    migrate_mock = Mock(return_value=True)
    monkeypatch.setattr(storage_system, "migrate_from_legacy_system", migrate_mock)
    monkeypatch.setattr(
        storage_system,
        "get_unified_preset_list",
        lambda: {
            "BlendPreset": ({"value": "blend"}, "BLEND_FILE"),
            "PersistentPreset": ({"value": "persistent"}, "PERSISTENT_FILE"),
        },
    )

    scene = make_scene(props)
    storage_system.initialize_presets()

    assert migrate_mock.called
    assert props.presets.cleared is True
    assert len(props.presets) == 2
    entries = {(item.name, item.source) for item in props.presets}
    assert ("BlendPreset", "BLEND_FILE") in entries
    assert ("PersistentPreset", "PERSISTENT_FILE") in entries
    assert "text_texture_presets" not in scene  # ensure scene custom data untouched


def test_ensure_presets_available_refreshes_when_empty(make_scene, monkeypatch):
    props = MockProps()
    make_scene(props)
    refresh_mock = Mock(return_value=True)
    monkeypatch.setattr(storage_system, "refresh_presets_sync", refresh_mock)

    result = storage_system.ensure_presets_available()

    assert result is True
    refresh_mock.assert_called_once()


def test_ensure_presets_available_returns_true_when_presets_loaded(make_scene, monkeypatch):
    props = MockProps()
    props.presets.add().name = "cached"
    make_scene(props)
    refresh_mock = Mock(return_value=True)
    monkeypatch.setattr(storage_system, "refresh_presets_sync", refresh_mock)

    result = storage_system.ensure_presets_available()

    assert result is True
    refresh_mock.assert_not_called()


def test_refresh_presets_sync_returns_false_without_scene(mock_bpy_fixture):
    mock_bpy_fixture.context.scene = None
    assert storage_system.refresh_presets_sync() is False
