"""
Tests for memory leak prevention in interval pool.

This test module verifies that touch operations don't cause memory leaks by:
1. Reusing existing interval dicts (Python references, not copies)
2. Dead intervals being cleaned up by GC
3. Serialization filtering out dead intervals from storage

NOTE: These tests are currently skipped due to the interval pool refactoring.
The tests access internal attributes (_fetch_groups, _timestamp_index, _gc_cleanup_dead_intervals)
that were part of the old monolithic pool.py implementation. After the refactoring into
separate modules (cache.py, index.py, garbage_collector.py, fetcher.py, manager.py),
these internal APIs changed and the tests need to be rewritten.

TODO: Rewrite these tests to work with the new modular architecture:
- Mock the api parameter (TibberPricesApiClient)
- Use public APIs instead of accessing internal attributes
- Test garbage collection through the manager's public interface
"""

import json
from datetime import UTC, datetime

import pytest

from custom_components.tibber_prices.interval_pool import TibberPricesIntervalPool

# Skip all tests in this module until they are rewritten for the new modular architecture
pytestmark = pytest.mark.skip(reason="Tests need rewrite for modular architecture (manager/cache/index/gc/fetcher)")


@pytest.fixture
def pool() -> TibberPricesIntervalPool:
    """Create a shared interval pool for testing (single-home architecture)."""
    return TibberPricesIntervalPool(home_id="test_home_id")


@pytest.fixture
def sample_intervals() -> list[dict]:
    """Create 24 sample intervals (1 day)."""
    base_time = datetime(2025, 11, 25, 0, 0, 0, tzinfo=UTC)
    return [
        {
            "startsAt": (base_time.replace(hour=h)).isoformat(),
            "total": 10.0 + h,
            "energy": 8.0 + h,
            "tax": 2.0,
        }
        for h in range(24)
    ]


def test_touch_operation_reuses_existing_intervals(
    pool: TibberPricesIntervalPool,
) -> None:
    """Test that touch operations reuse existing interval dicts (references, not copies)."""
    # home_id not needed (single-home architecture)
    fetch_time_1 = "2025-11-25T10:00:00+01:00"
    fetch_time_2 = "2025-11-25T10:15:00+01:00"

    # Create sample intervals for this test
    sample_intervals = [
        {
            "startsAt": datetime(2025, 11, 25, h, 0, 0, tzinfo=UTC).isoformat(),
            "total": 10.0 + h,
        }
        for h in range(24)
    ]

    # First fetch: Add intervals
    pool._add_intervals(sample_intervals, fetch_time_1)  # noqa: SLF001

    # Direct property access (single-home architecture)
    fetch_groups = pool._fetch_groups  # noqa: SLF001

    # Verify: 1 fetch group with 24 intervals
    assert len(fetch_groups) == 1
    assert len(fetch_groups[0]["intervals"]) == 24

    # Get reference to first interval
    first_interval_original = fetch_groups[0]["intervals"][0]
    original_id = id(first_interval_original)

    # Second fetch: Touch same intervals
    pool._add_intervals(sample_intervals, fetch_time_2)  # noqa: SLF001

    # Verify: Now we have 2 fetch groups
    assert len(fetch_groups) == 2

    # Get reference to first interval from TOUCH group
    first_interval_touched = fetch_groups[1]["intervals"][0]
    touched_id = id(first_interval_touched)

    # CRITICAL: Should be SAME object (same memory address)
    assert original_id == touched_id, f"Memory addresses differ: {original_id} != {touched_id}"
    assert first_interval_original is first_interval_touched, "Touch should reuse existing dict, not create copy"


def test_touch_operation_leaves_dead_intervals_in_old_group(
    pool: TibberPricesIntervalPool,
) -> None:
    """Test that touch operations leave 'dead' intervals in old fetch groups."""
    # home_id not needed (single-home architecture)
    fetch_time_1 = "2025-11-25T10:00:00+01:00"
    fetch_time_2 = "2025-11-25T10:15:00+01:00"

    # Create sample intervals
    sample_intervals = [
        {
            "startsAt": datetime(2025, 11, 25, h, 0, 0, tzinfo=UTC).isoformat(),
            "total": 10.0 + h,
        }
        for h in range(24)
    ]

    # First fetch
    pool._add_intervals(sample_intervals, fetch_time_1)  # noqa: SLF001
    # Direct property access (single-home architecture)
    fetch_groups = pool._fetch_groups  # noqa: SLF001

    # Second fetch (touch all intervals)
    pool._add_intervals(sample_intervals, fetch_time_2)  # noqa: SLF001

    # BEFORE GC cleanup:
    # - Old group still has 24 intervals (but they're all "dead" - index points elsewhere)
    # - Touch group has 24 intervals (living - index points here)

    assert len(fetch_groups) == 2, "Should have 2 fetch groups"
    assert len(fetch_groups[0]["intervals"]) == 24, "Old group should still have intervals (dead)"
    assert len(fetch_groups[1]["intervals"]) == 24, "Touch group should have intervals (living)"

    # Verify index points to touch group (not old group)
    timestamp_index = pool._timestamp_index  # noqa: SLF001
    first_key = sample_intervals[0]["startsAt"][:19]
    index_entry = timestamp_index[first_key]

    assert index_entry["fetch_group_index"] == 1, "Index should point to touch group"


def test_gc_cleanup_removes_dead_intervals(
    pool: TibberPricesIntervalPool,
) -> None:
    """Test that GC cleanup removes dead intervals from old fetch groups."""
    # home_id not needed (single-home architecture)
    fetch_time_1 = "2025-11-25T10:00:00+01:00"
    fetch_time_2 = "2025-11-25T10:15:00+01:00"

    # Create sample intervals
    sample_intervals = [
        {
            "startsAt": datetime(2025, 11, 25, h, 0, 0, tzinfo=UTC).isoformat(),
            "total": 10.0 + h,
        }
        for h in range(24)
    ]

    # First fetch
    pool._add_intervals(sample_intervals, fetch_time_1)  # noqa: SLF001

    # Second fetch (touch all intervals)
    pool._add_intervals(sample_intervals, fetch_time_2)  # noqa: SLF001

    # Direct property access (single-home architecture)
    fetch_groups = pool._fetch_groups  # noqa: SLF001
    timestamp_index = pool._timestamp_index  # noqa: SLF001

    # Before cleanup: old group has 24 intervals
    assert len(fetch_groups[0]["intervals"]) == 24, "Before cleanup"

    # Run GC cleanup explicitly
    dead_count = pool._gc_cleanup_dead_intervals(fetch_groups, timestamp_index)  # noqa: SLF001

    # Verify: 24 dead intervals were removed
    assert dead_count == 24, f"Expected 24 dead intervals, got {dead_count}"

    # After cleanup: old group should be empty
    assert len(fetch_groups[0]["intervals"]) == 0, "Old group should be empty after cleanup"

    # Touch group still has 24 living intervals
    assert len(fetch_groups[1]["intervals"]) == 24, "Touch group should still have intervals"


def test_serialization_excludes_dead_intervals(
    pool: TibberPricesIntervalPool,
) -> None:
    """Test that to_dict() excludes dead intervals from serialization."""
    # home_id not needed (single-home architecture)
    fetch_time_1 = "2025-11-25T10:00:00+01:00"
    fetch_time_2 = "2025-11-25T10:15:00+01:00"

    # Create sample intervals
    sample_intervals = [
        {
            "startsAt": datetime(2025, 11, 25, h, 0, 0, tzinfo=UTC).isoformat(),
            "total": 10.0 + h,
        }
        for h in range(24)
    ]

    # First fetch
    pool._add_intervals(sample_intervals, fetch_time_1)  # noqa: SLF001

    # Second fetch (touch all intervals)
    pool._add_intervals(sample_intervals, fetch_time_2)  # noqa: SLF001

    # Serialize WITHOUT running GC cleanup first
    serialized = pool.to_dict()

    # Verify serialization structure
    assert "fetch_groups" in serialized
    assert "home_id" in serialized
    fetch_groups = serialized["fetch_groups"]

    # CRITICAL: Should only serialize touch group (living intervals)
    # Old group with all dead intervals should NOT be serialized
    assert len(fetch_groups) == 1, "Should only serialize groups with living intervals"

    # Touch group should have all 24 intervals
    assert len(fetch_groups[0]["intervals"]) == 24, "Touch group should have all intervals"

    # Verify JSON size is reasonable (not 2x the size)
    json_str = json.dumps(serialized)
    json_size = len(json_str)
    # Each interval is ~100-150 bytes, 24 intervals = ~2.4-3.6 KB
    # With metadata + structure, expect < 5 KB
    assert json_size < 5000, f"JSON too large: {json_size} bytes (expected < 5000)"


def test_repeated_touch_operations_dont_grow_storage(
    pool: TibberPricesIntervalPool,
) -> None:
    """Test that repeated touch operations don't grow storage size unbounded."""
    # home_id not needed (single-home architecture)

    # Create sample intervals
    sample_intervals = [
        {
            "startsAt": datetime(2025, 11, 25, h, 0, 0, tzinfo=UTC).isoformat(),
            "total": 10.0 + h,
        }
        for h in range(24)
    ]

    # Simulate 10 re-fetches of the same intervals
    for i in range(10):
        fetch_time = f"2025-11-25T{10 + i}:00:00+01:00"
        pool._add_intervals(sample_intervals, fetch_time)  # noqa: SLF001

    # Memory state: 10 fetch groups (9 empty, 1 with all intervals)
    # Direct property access (single-home architecture)
    fetch_groups = pool._fetch_groups  # noqa: SLF001
    assert len(fetch_groups) == 10, "Should have 10 fetch groups in memory"

    # Total intervals in memory: 240 references (24 per group, mostly dead)
    total_refs = sum(len(g["intervals"]) for g in fetch_groups)
    assert total_refs == 24 * 10, "Memory should have 240 interval references"

    # Serialize (filters dead intervals)
    serialized = pool.to_dict()
    serialized_groups = serialized["fetch_groups"]

    # Storage should only have 1 group with 24 living intervals
    assert len(serialized_groups) == 1, "Should only serialize 1 group (with living intervals)"
    assert len(serialized_groups[0]["intervals"]) == 24, "Should only have 24 living intervals"

    # Verify storage size is bounded
    json_str = json.dumps(serialized)
    json_size = len(json_str)
    # Should still be < 10 KB even after 10 fetches
    assert json_size < 10000, f"Storage grew unbounded: {json_size} bytes (expected < 10000)"


def test_gc_cleanup_with_partial_touch(
    pool: TibberPricesIntervalPool,
    sample_intervals: list[dict],
) -> None:
    """Test GC cleanup when only some intervals are touched (partial overlap)."""
    # home_id not needed (single-home architecture)
    fetch_time_1 = "2025-11-25T10:00:00+01:00"
    fetch_time_2 = "2025-11-25T10:15:00+01:00"

    # First fetch: All 24 intervals
    pool._add_intervals(sample_intervals, fetch_time_1)  # noqa: SLF001

    # Second fetch: Only first 12 intervals (partial touch)
    partial_intervals = sample_intervals[:12]
    pool._add_intervals(partial_intervals, fetch_time_2)  # noqa: SLF001

    # Direct property access (single-home architecture)
    fetch_groups = pool._fetch_groups  # noqa: SLF001
    timestamp_index = pool._timestamp_index  # noqa: SLF001

    # Before cleanup:
    # - Old group: 24 intervals (12 dead, 12 living)
    # - Touch group: 12 intervals (all living)
    assert len(fetch_groups[0]["intervals"]) == 24, "Old group should have 24 intervals"
    assert len(fetch_groups[1]["intervals"]) == 12, "Touch group should have 12 intervals"

    # Run GC cleanup
    dead_count = pool._gc_cleanup_dead_intervals(fetch_groups, timestamp_index)  # noqa: SLF001

    # Should clean 12 dead intervals (the ones that were touched)
    assert dead_count == 12, f"Expected 12 dead intervals, got {dead_count}"

    # After cleanup:
    # - Old group: 12 intervals (the ones that were NOT touched)
    # - Touch group: 12 intervals (unchanged)
    assert len(fetch_groups[0]["intervals"]) == 12, "Old group should have 12 living intervals left"
    assert len(fetch_groups[1]["intervals"]) == 12, "Touch group should still have 12 intervals"


def test_memory_leak_prevention_integration(
    pool: TibberPricesIntervalPool,
) -> None:
    """Integration test: Verify no memory leak over multiple operations."""
    # home_id not needed (single-home architecture)

    # Create sample intervals
    sample_intervals = [
        {
            "startsAt": datetime(2025, 11, 25, h, 0, 0, tzinfo=UTC).isoformat(),
            "total": 10.0 + h,
        }
        for h in range(24)
    ]

    # Simulate typical usage pattern over time
    # Day 1: Fetch 24 intervals
    pool._add_intervals(sample_intervals, "2025-11-25T10:00:00+01:00")  # noqa: SLF001

    # Day 1: Re-fetch (touch) - updates fetch time
    pool._add_intervals(sample_intervals, "2025-11-25T14:00:00+01:00")  # noqa: SLF001

    # Day 1: Re-fetch (touch) again
    pool._add_intervals(sample_intervals, "2025-11-25T18:00:00+01:00")  # noqa: SLF001

    # Direct property access (single-home architecture)
    fetch_groups = pool._fetch_groups  # noqa: SLF001
    timestamp_index = pool._timestamp_index  # noqa: SLF001

    # Memory state BEFORE cleanup:
    # - 3 fetch groups
    # - Total: 72 interval references (24 per group)
    # - Dead: 48 (first 2 groups have all dead intervals)
    # - Living: 24 (last group has all living intervals)
    assert len(fetch_groups) == 3, "Should have 3 fetch groups"
    total_refs = sum(len(g["intervals"]) for g in fetch_groups)
    assert total_refs == 72, "Should have 72 interval references in memory"

    # Run GC cleanup
    dead_count = pool._gc_cleanup_dead_intervals(fetch_groups, timestamp_index)  # noqa: SLF001
    assert dead_count == 48, "Should clean 48 dead intervals"

    # Memory state AFTER cleanup:
    # - 3 fetch groups (2 empty, 1 with all intervals)
    # - Total: 24 interval references
    # - Dead: 0
    # - Living: 24
    total_refs_after = sum(len(g["intervals"]) for g in fetch_groups)
    assert total_refs_after == 24, "Should only have 24 interval references after cleanup"

    # Verify serialization excludes empty groups
    serialized = pool.to_dict()
    serialized_groups = serialized["fetch_groups"]

    # Should only serialize 1 group (the one with living intervals)
    assert len(serialized_groups) == 1, "Should only serialize groups with living intervals"
    assert len(serialized_groups[0]["intervals"]) == 24, "Should have 24 intervals"


def test_interval_identity_preserved_across_touch(
    pool: TibberPricesIntervalPool,
) -> None:
    """Test that interval dict identity (memory address) is preserved across touch."""
    # home_id not needed (single-home architecture)

    # Create sample intervals
    sample_intervals = [
        {
            "startsAt": datetime(2025, 11, 25, h, 0, 0, tzinfo=UTC).isoformat(),
            "total": 10.0 + h,
        }
        for h in range(24)
    ]

    # First fetch
    pool._add_intervals(sample_intervals, "2025-11-25T10:00:00+01:00")  # noqa: SLF001

    # Direct property access (single-home architecture)
    fetch_groups = pool._fetch_groups  # noqa: SLF001

    # Collect memory addresses of intervals in original group
    original_ids = [id(interval) for interval in fetch_groups[0]["intervals"]]

    # Second fetch (touch)
    pool._add_intervals(sample_intervals, "2025-11-25T10:15:00+01:00")  # noqa: SLF001

    # Collect memory addresses of intervals in touch group
    touched_ids = [id(interval) for interval in fetch_groups[1]["intervals"]]

    # CRITICAL: All memory addresses should be identical (same objects)
    assert original_ids == touched_ids, "Touch should preserve interval identity (memory addresses)"

    # Third fetch (touch again)
    pool._add_intervals(sample_intervals, "2025-11-25T10:30:00+01:00")  # noqa: SLF001

    # New touch group should also reference the SAME original objects
    touched_ids_2 = [id(interval) for interval in fetch_groups[2]["intervals"]]
    assert original_ids == touched_ids_2, "Multiple touches should preserve original identity"

    # Verify: All 3 groups have references to THE SAME interval dicts
    # Only the list entries differ (8 bytes each), not the interval dicts (600+ bytes each)
    for i in range(24):
        assert fetch_groups[0]["intervals"][i] is fetch_groups[1]["intervals"][i] is fetch_groups[2]["intervals"][i], (
            f"Interval {i} should be the same object across all groups"
        )
