"""Integration tests for cache validation (spot check) logic."""

import numpy as np
import pandas as pd

from dyfinance._cache import CacheEngine
from dyfinance._ticker import Ticker, ValidationResult

# Use fixed date range that matches fixture data
_START = "2024-01-02"
_END = "2024-01-09"


def _make_raw_df(adj_close_values, start=_START, tz="America/New_York"):
    """Create a yfinance-style raw DataFrame with given Adj Close values."""
    n = len(adj_close_values)
    dates = pd.date_range(start, periods=n, freq="B", tz=tz)
    return pd.DataFrame(
        {
            "Open": [100.0] * n,
            "High": [101.0] * n,
            "Low": [99.0] * n,
            "Close": [100.5] * n,
            "Adj Close": adj_close_values,
            "Volume": [1000000] * n,
            "Dividends": [0.0] * n,
            "Stock Splits": [0.0] * n,
            "Capital Gains": [0.0] * n,
        },
        index=dates,
    )


def _age_cache(cache: CacheEngine, ticker: str = "AAPL", interval: str = "1d") -> None:
    """Set fetched_at to a very old date to trigger spot check."""
    cache._con.execute(
        "UPDATE cache_meta SET fetched_at = '2020-01-01 00:00:00+00' "
        f"WHERE ticker = '{ticker}' AND interval = '{interval}'",
    )


class TestSpotCheckFreshCache:
    """Spot check should be skipped when cache is fresh."""

    def test_fresh_cache_skips_yfinance_call(self, mock_yf_ticker):
        """When fetched_at is recent, history() should NOT call yfinance for spot check."""
        raw_df = _make_raw_df([100.5, 101.5, 102.5, 103.5, 104.5])
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")

        # First call: cache miss → fetches from yfinance
        t.history(start=_START, end=_END, interval="1d")
        first_call_count = mock_yf_ticker.history.call_count

        # Second call: query a subset within the cached range
        t.history(start="2024-01-03", end="2024-01-08", interval="1d")
        assert mock_yf_ticker.history.call_count == first_call_count


class TestSpotCheckStaleMatch:
    """Spot check with matching Adj Close should only update fetched_at."""

    def test_stale_cache_match_updates_fetched_at(self, mock_yf_ticker):
        """When cache is stale but Adj Close matches, fetched_at should be refreshed."""
        raw_df = _make_raw_df([100.5, 101.5, 102.5, 103.5, 104.5])
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        cache = CacheEngine.default()

        # Populate cache
        t.history(start=_START, end=_END, interval="1d")

        # Artificially age the cache
        _age_cache(cache)

        # Spot check should find a match and refresh fetched_at
        t.history(start=_START, end=_END, interval="1d")

        fetched_at = cache.get_fetched_at("AAPL", "1d")
        assert fetched_at is not None
        assert fetched_at.year > 2020


class TestSpotCheckUniformRatio:
    """Spot check with uniform Adj Close ratio should correct without invalidating."""

    def test_uniform_ratio_applies_correction(self, mock_yf_ticker):
        """A stock split should be detected and Adj Close corrected by uniform ratio."""
        original_adj = [100.5, 101.5, 102.5, 103.5, 104.5]
        raw_df = _make_raw_df(original_adj)
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        cache = CacheEngine.default()

        # Populate cache
        t.history(start=_START, end=_END, interval="1d")

        # Artificially age the cache
        _age_cache(cache)

        # Simulate a 2:1 stock split: yfinance now returns halved Adj Close
        split_adj = [v * 0.5 for v in original_adj]
        split_df = _make_raw_df(split_adj)
        mock_yf_ticker.history.return_value = split_df

        # Spot check should detect uniform ratio and correct
        t.history(start=_START, end=_END, interval="1d")

        # Verify the cached Adj Close was corrected
        cached_adj = cache.get_cached_adj_close("AAPL", "1d", n=5)
        np.testing.assert_allclose(cached_adj.values, split_adj, rtol=1e-4)

    def test_uniform_ratio_returns_corrected_values(self, mock_yf_ticker):
        """The history() call that triggers ratio correction must return corrected values."""
        original_adj = [100.5, 101.5, 102.5, 103.5, 104.5]
        raw_df = _make_raw_df(original_adj)
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        cache = CacheEngine.default()

        # Populate cache
        t.history(start=_START, end=_END, interval="1d")

        # Artificially age the cache
        _age_cache(cache)

        # Simulate a 2:1 stock split: yfinance now returns halved Adj Close
        split_adj = [v * 0.5 for v in original_adj]
        split_df = _make_raw_df(split_adj)
        mock_yf_ticker.history.return_value = split_df

        # Call with auto_adjust=False to get Adj Close in the returned DataFrame
        result = t.history(start=_START, end=_END, interval="1d", auto_adjust=False)

        # The RETURNED DataFrame must have the corrected Adj Close, not stale values
        np.testing.assert_allclose(result["Adj Close"].values, split_adj, rtol=1e-4)


class TestSpotCheckNonUniformRatio:
    """Spot check with non-uniform Adj Close changes should invalidate cache."""

    def test_non_uniform_ratio_invalidates_cache(self, mock_yf_ticker):
        """Non-uniform Adj Close changes should trigger full cache invalidation."""
        original_adj = [100.5, 101.5, 102.5, 103.5, 104.5]
        raw_df = _make_raw_df(original_adj)
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        cache = CacheEngine.default()

        # Populate cache
        t.history(start=_START, end=_END, interval="1d")

        # Artificially age the cache
        _age_cache(cache)

        # Simulate non-uniform changes (data correction, not a split)
        corrupted_adj = [50.0, 101.5, 200.0, 103.5, 55.0]
        corrupted_df = _make_raw_df(corrupted_adj)
        mock_yf_ticker.history.return_value = corrupted_df

        # Spot check should invalidate and re-fetch
        t.history(start=_START, end=_END, interval="1d")

        # Cache should now contain the fresh (corrupted) data
        cached_adj = cache.get_cached_adj_close("AAPL", "1d", n=5)
        np.testing.assert_allclose(cached_adj.values, corrupted_adj, rtol=1e-4)


class TestValidationResultDirect:
    """Directly test _validate_cached_adj_close return type."""

    def test_fresh_cache_returns_ok(self, mock_yf_ticker):
        raw_df = _make_raw_df([100.5, 101.5, 102.5, 103.5, 104.5])
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        t.history(start=_START, end=_END, interval="1d")

        result = t._validate_cached_adj_close("AAPL", "1d", 10, False)
        assert result is ValidationResult.OK

    def test_stale_matching_returns_ok(self, mock_yf_ticker):
        raw_df = _make_raw_df([100.5, 101.5, 102.5, 103.5, 104.5])
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        cache = CacheEngine.default()
        t.history(start=_START, end=_END, interval="1d")
        _age_cache(cache)

        result = t._validate_cached_adj_close("AAPL", "1d", 10, False)
        assert result is ValidationResult.OK

    def test_uniform_ratio_returns_corrected(self, mock_yf_ticker):
        original_adj = [100.5, 101.5, 102.5, 103.5, 104.5]
        raw_df = _make_raw_df(original_adj)
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        cache = CacheEngine.default()
        t.history(start=_START, end=_END, interval="1d")
        _age_cache(cache)

        split_adj = [v * 0.5 for v in original_adj]
        mock_yf_ticker.history.return_value = _make_raw_df(split_adj)

        result = t._validate_cached_adj_close("AAPL", "1d", 10, False)
        assert result is ValidationResult.CORRECTED

    def test_non_uniform_returns_invalidated(self, mock_yf_ticker):
        raw_df = _make_raw_df([100.5, 101.5, 102.5, 103.5, 104.5])
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        cache = CacheEngine.default()
        t.history(start=_START, end=_END, interval="1d")
        _age_cache(cache)

        corrupted_adj = [50.0, 101.5, 200.0, 103.5, 55.0]
        mock_yf_ticker.history.return_value = _make_raw_df(corrupted_adj)

        result = t._validate_cached_adj_close("AAPL", "1d", 10, False)
        assert result is ValidationResult.INVALIDATED


class TestClearCacheViaTicker:
    """Ticker.clear_cache() should delegate to CacheEngine.invalidate()."""

    def test_clear_cache_removes_data(self, mock_yf_ticker):
        raw_df = _make_raw_df([100.5, 101.5, 102.5, 103.5, 104.5])
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        cache = CacheEngine.default()

        # Populate cache
        t.history(start=_START, end=_END, interval="1d")
        assert cache.get_fetched_at("AAPL", "1d") is not None

        # Clear and verify
        t.clear_cache()
        assert cache.get_fetched_at("AAPL", "1d") is None

    def test_clear_cache_specific_interval(self, mock_yf_ticker):
        raw_df = _make_raw_df([100.5, 101.5, 102.5, 103.5, 104.5])
        mock_yf_ticker.history.return_value = raw_df

        t = Ticker("AAPL")
        cache = CacheEngine.default()

        # Populate both 1d and 1wk
        t.history(start=_START, end=_END, interval="1d")
        mock_yf_ticker.history.return_value = raw_df
        t.history(start=_START, end=_END, interval="1wk")

        # Clear only 1d
        t.clear_cache("1d")
        assert cache.get_fetched_at("AAPL", "1d") is None
        assert cache.get_fetched_at("AAPL", "1wk") is not None
