"""Unit tests for LinearLocalReader cache refresh and failure-resilience logic."""

import time
from unittest.mock import MagicMock

from oh_my_linear.reader import CachedData, LinearLocalReader


class TestCacheDataStructure:
    """Tests for CachedData structure and initialization."""

    def test_cached_data_default_fields(self):
        """CachedData initializes with empty collections."""
        cache = CachedData()
        assert cache.teams == {}
        assert cache.users == {}
        assert cache.issues == {}
        assert cache.comments == {}
        assert cache.projects == {}
        assert cache.loaded_at == 0.0

    def test_cached_data_with_sample_data(self):
        """CachedData can store teams and users."""
        cache = CachedData(
            teams={"t1": {"id": "t1", "name": "Team 1"}},
            users={"u1": {"id": "u1", "name": "User 1"}},
        )
        assert cache.teams["t1"]["name"] == "Team 1"
        assert cache.users["u1"]["name"] == "User 1"


class TestReaderInitialization:
    """Tests for LinearLocalReader initialization."""

    def test_reader_initializes_with_default_cache(self):
        """LinearLocalReader initializes with empty CachedData."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        assert isinstance(reader._cache, CachedData)
        assert reader._cache.teams == {}

    def test_reader_stores_db_paths(self):
        """LinearLocalReader stores db_path and blob_path."""
        db = "/test/db"
        blob = "/test/blob"
        reader = LinearLocalReader(db_path=db, blob_path=blob)
        assert reader._db_path == db
        assert reader._blob_path == blob


class TestRefreshCachePolicy:
    """Tests for conditional refresh policy."""

    def test_refresh_cache_calls_reload_on_cold_start(self):
        """Cold start should reload because no cache is loaded yet."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        reader._reload_cache = MagicMock()

        reader.refresh_cache()

        reader._reload_cache.assert_called_once()

    def test_refresh_cache_skips_reload_when_cache_is_fresh_and_unchanged(self):
        """Fresh cache with unchanged source should not trigger reload."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        reader._cache.loaded_at = time.time()
        reader._reload_cache = MagicMock()
        reader._latest_source_mtime = MagicMock(return_value=reader._cache.loaded_at)  # type: ignore[attr-defined]

        reader.refresh_cache()

        reader._reload_cache.assert_not_called()

    def test_refresh_cache_reloads_when_ttl_expired(self):
        """Expired TTL should trigger reload."""
        reader = LinearLocalReader(
            db_path="/nonexistent",
            blob_path="/nonexistent",
            refresh_ttl_seconds=1,
        )
        reader._cache.loaded_at = time.time() - 10
        reader._reload_cache = MagicMock()
        reader._latest_source_mtime = MagicMock(return_value=None)  # type: ignore[attr-defined]

        reader.refresh_cache()

        reader._reload_cache.assert_called_once()

    def test_refresh_cache_reloads_when_source_newer_than_cache(self):
        """Recent local DB writes should bypass TTL and trigger reload."""
        reader = LinearLocalReader(
            db_path="/nonexistent",
            blob_path="/nonexistent",
            refresh_ttl_seconds=3600,
        )
        reader._cache.loaded_at = time.time()
        reader._reload_cache = MagicMock()
        reader._latest_source_mtime = MagicMock(return_value=reader._cache.loaded_at + 5)  # type: ignore[attr-defined]

        reader.refresh_cache()

        reader._reload_cache.assert_called_once()

    def test_refresh_cache_force_reloads_even_when_fresh(self):
        """force=True should always trigger reload."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        reader._cache.loaded_at = time.time()
        reader._reload_cache = MagicMock()
        reader._latest_source_mtime = MagicMock(return_value=reader._cache.loaded_at)  # type: ignore[attr-defined]

        reader.refresh_cache(force=True)

        reader._reload_cache.assert_called_once()


class TestCachedDataAvailability:
    """Tests for cache availability checks."""

    def test_has_cached_data_false_for_empty_cache(self):
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        reader._cache = CachedData()
        assert reader.has_cached_data() is False

    def test_has_cached_data_true_when_primary_entities_exist(self):
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        reader._cache = CachedData(teams={"t1": {"id": "t1"}})
        assert reader.has_cached_data() is True


class TestFailurePreservesPreviousCache:
    """Tests that _reload_cache failure preserves previous cache."""

    def test_reload_failure_keeps_previous_cache(self):
        """When _reload_cache fails, self._cache is preserved."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")

        # Set up a known previous cache
        previous_cache = CachedData(
            loaded_at=1000.0,
            teams={"t1": {"id": "t1", "name": "Team 1"}},
        )
        reader._cache = previous_cache

        # Call refresh_cache which will fail because db_path doesn't exist
        reader.refresh_cache()

        # Previous cache should be preserved
        assert reader._cache.teams == {"t1": {"id": "t1", "name": "Team 1"}}
        assert reader._cache.loaded_at == 1000.0

    def test_reload_failure_sets_degraded(self):
        """When _reload_cache fails, health is set to degraded."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")

        previous_cache = CachedData(
            loaded_at=1000.0,
            teams={"t1": {"id": "t1"}},
        )
        reader._cache = previous_cache

        reader.refresh_cache()

        assert reader.is_degraded() is True


class TestPropertiesReturnCacheDirectly:
    """Tests that properties return self._cache fields directly."""

    def test_teams_property(self):
        """Teams property returns self._cache.teams."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        reader._cache = CachedData(teams={"t1": {"id": "t1"}})
        assert reader.teams == {"t1": {"id": "t1"}}

    def test_users_property(self):
        """Users property returns self._cache.users."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        reader._cache = CachedData(users={"u1": {"id": "u1"}})
        assert reader.users == {"u1": {"id": "u1"}}

    def test_issues_property(self):
        """Issues property returns self._cache.issues."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        reader._cache = CachedData(issues={"i1": {"id": "i1"}})
        assert reader.issues == {"i1": {"id": "i1"}}

    def test_empty_cache_returns_empty(self):
        """Properties return empty dicts from default cache."""
        reader = LinearLocalReader(db_path="/nonexistent", blob_path="/nonexistent")
        assert reader.teams == {}
        assert reader.users == {}
        assert reader.issues == {}
