"""Tests for ticker metadata cache (get_ticker_name_dicts)."""

from __future__ import annotations

import json
import os
import time
from unittest.mock import MagicMock, patch

import pytest
from secbulkdownload.utils import get_ticker_name_dicts

# Minimal sample payload mimicking SEC company_tickers.json structure.
SAMPLE_TICKERS_JSON: dict[str, dict[str, object]] = {
    "0": {"cik_str": 320193, "ticker": "AAPL", "title": "Apple Inc."},
    "1": {"cik_str": 789019, "ticker": "MSFT", "title": "Microsoft Corp"},
    "2": {"cik_str": 1018724, "ticker": "AMZN", "title": "Amazon Com Inc"},
}


def _mock_urlopen(sample: dict[str, dict[str, object]]) -> MagicMock:
    """Return a context-manager mock for ``urllib.request.urlopen``."""
    response = MagicMock()
    response.read.return_value = json.dumps(sample).encode()
    response.__enter__ = lambda s: s
    response.__exit__ = MagicMock(return_value=False)
    return response


# ---------- Contract tests ----------


@patch("secbulkdownload.utils.urllib.request.urlopen")
def test_return_contract(mock_urlopen: MagicMock, tmp_path: pytest.TempPathFactory) -> None:
    """get_ticker_name_dicts returns a 2-tuple of dicts."""
    mock_urlopen.return_value = _mock_urlopen(SAMPLE_TICKERS_JSON)
    result = get_ticker_name_dicts(datadir=str(tmp_path))

    assert isinstance(result, tuple)
    assert len(result) == 2
    ticker_dict, name_dict = result
    assert isinstance(ticker_dict, dict)
    assert isinstance(name_dict, dict)


@patch("secbulkdownload.utils.urllib.request.urlopen")
def test_cik_keys_zero_padded(mock_urlopen: MagicMock, tmp_path: pytest.TempPathFactory) -> None:
    """All CIK keys must be zero-padded 10-character strings."""
    mock_urlopen.return_value = _mock_urlopen(SAMPLE_TICKERS_JSON)
    ticker_dict, name_dict = get_ticker_name_dicts(datadir=str(tmp_path))

    for key in ticker_dict:
        assert isinstance(key, str)
        assert len(key) == 10
        assert key == key.zfill(10)

    for key in name_dict:
        assert isinstance(key, str)
        assert len(key) == 10
        assert key == key.zfill(10)


@patch("secbulkdownload.utils.urllib.request.urlopen")
def test_ticker_values_are_strings(mock_urlopen: MagicMock, tmp_path: pytest.TempPathFactory) -> None:
    """ticker_dict values must be ticker symbol strings."""
    mock_urlopen.return_value = _mock_urlopen(SAMPLE_TICKERS_JSON)
    ticker_dict, _ = get_ticker_name_dicts(datadir=str(tmp_path))

    for val in ticker_dict.values():
        assert isinstance(val, str)

    assert "AAPL" in ticker_dict.values()
    assert "MSFT" in ticker_dict.values()


@patch("secbulkdownload.utils.urllib.request.urlopen")
def test_name_values_are_strings(mock_urlopen: MagicMock, tmp_path: pytest.TempPathFactory) -> None:
    """name_dict values must be company name strings."""
    mock_urlopen.return_value = _mock_urlopen(SAMPLE_TICKERS_JSON)
    _, name_dict = get_ticker_name_dicts(datadir=str(tmp_path))

    for val in name_dict.values():
        assert isinstance(val, str)

    assert "Apple Inc." in name_dict.values()


# ---------- Cache behaviour tests ----------


@patch("secbulkdownload.utils.urllib.request.urlopen")
def test_cache_reuse_offline(mock_urlopen: MagicMock, tmp_path: pytest.TempPathFactory) -> None:
    """After first call, a second call must succeed even with network disabled."""
    mock_urlopen.return_value = _mock_urlopen(SAMPLE_TICKERS_JSON)

    # First call populates cache
    t1, n1 = get_ticker_name_dicts(datadir=str(tmp_path))

    # Second call: simulate network failure
    mock_urlopen.side_effect = OSError("network unavailable")
    t2, n2 = get_ticker_name_dicts(datadir=str(tmp_path))

    assert t1 == t2
    assert n1 == n2


@patch("secbulkdownload.utils.urllib.request.urlopen")
def test_stale_cache_triggers_refresh(mock_urlopen: MagicMock, tmp_path: pytest.TempPathFactory) -> None:
    """A stale or missing cache must trigger a fresh fetch."""
    mock_urlopen.return_value = _mock_urlopen(SAMPLE_TICKERS_JSON)

    # First call creates cache
    get_ticker_name_dicts(datadir=str(tmp_path))

    # Make the cache file appear stale (>24h old)
    cache_path = os.path.join(str(tmp_path), "company_tickers.json")
    old_time = time.time() - 2 * 86400  # 2 days ago
    os.utime(cache_path, (old_time, old_time))

    # Prepare updated data for refresh
    updated: dict[str, dict[str, object]] = {
        "0": {"cik_str": 320193, "ticker": "AAPL", "title": "Apple Inc. UPDATED"},
    }
    mock_urlopen.return_value = _mock_urlopen(updated)

    ticker_dict, name_dict = get_ticker_name_dicts(datadir=str(tmp_path))

    # Must have new data, not the old cache
    cik_apple = str(320193).zfill(10)
    assert name_dict[cik_apple] == "Apple Inc. UPDATED"
    # Only one entry now
    assert len(ticker_dict) == 1


@patch("secbulkdownload.utils.urllib.request.urlopen")
def test_cache_file_written_in_datadir(mock_urlopen: MagicMock, tmp_path: pytest.TempPathFactory) -> None:
    """Cache file must be written inside the caller-provided datadir."""
    mock_urlopen.return_value = _mock_urlopen(SAMPLE_TICKERS_JSON)
    get_ticker_name_dicts(datadir=str(tmp_path))

    cache_path = os.path.join(str(tmp_path), "company_tickers.json")
    assert os.path.isfile(cache_path)

    # Verify it's valid JSON
    with open(cache_path) as f:
        data = json.load(f)
    assert isinstance(data, dict)
    assert len(data) == len(SAMPLE_TICKERS_JSON)
