"""
tests/test_stats_db.py — Tests for the shared StatsDB (#550).

Covers:
- Schema creation (initialize)
- record_request + query back
- get_usage_daily aggregation
- get_usage_summary monthly totals
- get_system_overview
- prune_old_requests
- Concurrent writes (WAL mode)
- Config field parsing
"""
from __future__ import annotations

import asyncio
import os
import tempfile
from datetime import date, timedelta
from typing import AsyncGenerator

import pytest
import pytest_asyncio

from cortex.api.stats_db import StatsDB


# ---------------------------------------------------------------------------
# Helpers / fixtures
# ---------------------------------------------------------------------------

@pytest_asyncio.fixture
async def db(tmp_path) -> AsyncGenerator[StatsDB, None]:
    """In-file temp StatsDB (WAL mode requires a real file, not :memory:)."""
    db_path = str(tmp_path / "stats_test.db")
    stats = StatsDB(db_path=db_path)
    await stats.initialize()
    yield stats
    await stats.close()


async def _insert_request(
    db: StatsDB,
    owner_id: str = "user1",
    endpoint: str = "/api/v1/remember",
    method: str = "POST",
    status_code: int = 200,
    latency_ms: float = 42.5,
    tokens_used: int = 10,
) -> None:
    await db.record_request(
        owner_id=owner_id,
        endpoint=endpoint,
        method=method,
        status_code=status_code,
        latency_ms=latency_ms,
        tokens_used=tokens_used,
    )


# ---------------------------------------------------------------------------
# Schema creation
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_initialize_creates_tables(tmp_path):
    """StatsDB.initialize() creates all required tables."""
    db_path = str(tmp_path / "init_test.db")
    stats = StatsDB(db_path=db_path)
    await stats.initialize()

    tables = set()
    async with stats._conn.execute(
        "SELECT name FROM sqlite_master WHERE type='table'"
    ) as cursor:
        async for row in cursor:
            tables.add(row[0])

    assert "request_log" in tables
    assert "usage_daily" in tables
    assert "system_metrics" in tables
    await stats.close()


@pytest.mark.asyncio
async def test_initialize_enables_wal(tmp_path):
    """WAL journal mode is set during initialization."""
    db_path = str(tmp_path / "wal_test.db")
    stats = StatsDB(db_path=db_path)
    await stats.initialize()

    async with stats._conn.execute("PRAGMA journal_mode") as cursor:
        row = await cursor.fetchone()
    assert row[0] == "wal"
    await stats.close()


@pytest.mark.asyncio
async def test_initialize_idempotent(tmp_path):
    """Calling initialize() twice does not raise (IF NOT EXISTS)."""
    db_path = str(tmp_path / "idem_test.db")
    stats = StatsDB(db_path=db_path)
    await stats.initialize()
    await stats.initialize()  # second call — should be fine
    await stats.close()


# ---------------------------------------------------------------------------
# record_request + query back
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_record_request_inserts_row(db: StatsDB):
    """record_request writes a row to request_log."""
    await _insert_request(db, owner_id="alice", latency_ms=55.1, tokens_used=20)

    async with db._conn.execute(
        "SELECT owner_id, endpoint, method, status_code, latency_ms, tokens_used FROM request_log"
    ) as cursor:
        rows = await cursor.fetchall()

    assert len(rows) == 1
    row = rows[0]
    assert row["owner_id"] == "alice"
    assert row["endpoint"] == "/api/v1/remember"
    assert row["method"] == "POST"
    assert row["status_code"] == 200
    assert abs(row["latency_ms"] - 55.1) < 0.01
    assert row["tokens_used"] == 20


@pytest.mark.asyncio
async def test_record_request_multiple_users(db: StatsDB):
    """Multiple users can record requests independently."""
    await _insert_request(db, owner_id="alice")
    await _insert_request(db, owner_id="bob")
    await _insert_request(db, owner_id="alice")

    async with db._conn.execute(
        "SELECT COUNT(*) AS cnt FROM request_log WHERE owner_id='alice'"
    ) as cursor:
        row = await cursor.fetchone()
    assert row["cnt"] == 2


@pytest.mark.asyncio
async def test_record_request_no_db_silently_no_ops():
    """record_request with no connection silently does nothing."""
    stats = StatsDB(db_path=":memory:")
    # Don't call initialize — _conn stays None
    await stats.record_request("u", "/ep", "GET", 200, 10.0)
    # Should not raise


# ---------------------------------------------------------------------------
# get_usage_daily
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_usage_daily_empty(db: StatsDB):
    """get_usage_daily returns empty list when usage_daily has no rows."""
    result = await db.get_usage_daily("nonexistent_user")
    assert result == []


@pytest.mark.asyncio
async def test_get_usage_daily_after_aggregate(db: StatsDB):
    """After aggregate_daily, get_usage_daily reflects rolled-up data."""
    await _insert_request(db, owner_id="alice", latency_ms=30.0, tokens_used=5)
    await _insert_request(db, owner_id="alice", latency_ms=50.0, tokens_used=15)
    await db.aggregate_daily()

    rows = await db.get_usage_daily("alice", days=7)
    assert len(rows) >= 1
    today = rows[0]
    assert today["call_count"] == 2
    assert today["tokens_total"] == 20
    assert today["avg_latency_ms"] is not None


# ---------------------------------------------------------------------------
# get_usage_summary
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_usage_summary_empty(db: StatsDB):
    """get_usage_summary returns zeros when no data exists."""
    result = await db.get_usage_summary("ghost_user")
    assert result["call_count"] == 0
    assert result["tokens_total"] == 0


@pytest.mark.asyncio
async def test_get_usage_summary_current_month(db: StatsDB):
    """get_usage_summary aggregates current month from usage_daily."""
    # Seed usage_daily directly
    today = date.today().isoformat()
    month = today[:7]
    await db._conn.execute(
        """
        INSERT INTO usage_daily (owner_id, date, call_count, tokens_total, avg_latency_ms)
        VALUES (?, ?, ?, ?, ?)
        """,
        ("alice", today, 100, 500, 40.0),
    )
    await db._conn.commit()

    result = await db.get_usage_summary("alice", month=month)
    assert result["call_count"] == 100
    assert result["tokens_total"] == 500
    assert result["period"] == month


@pytest.mark.asyncio
async def test_get_usage_summary_specific_month(db: StatsDB):
    """get_usage_summary filters to the specified month only."""
    last_month = (date.today().replace(day=1) - timedelta(days=1)).strftime("%Y-%m")
    last_month_date = f"{last_month}-15"
    this_month = date.today().strftime("%Y-%m")
    this_month_date = date.today().isoformat()

    await db._conn.executemany(
        "INSERT INTO usage_daily (owner_id, date, call_count, tokens_total, avg_latency_ms) VALUES (?,?,?,?,?)",
        [
            ("alice", last_month_date, 50, 200, 30.0),
            ("alice", this_month_date, 80, 400, 45.0),
        ],
    )
    await db._conn.commit()

    result = await db.get_usage_summary("alice", month=last_month)
    assert result["call_count"] == 50
    assert result["period"] == last_month


# ---------------------------------------------------------------------------
# get_system_overview
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_system_overview_empty(db: StatsDB):
    """get_system_overview returns zeros on empty DB."""
    result = await db.get_system_overview()
    assert result["total_users"] == 0
    assert result["total_requests_24h"] == 0
    assert result["active_users_24h"] == 0


@pytest.mark.asyncio
async def test_get_system_overview_with_data(db: StatsDB):
    """get_system_overview reflects recent request_log entries."""
    await _insert_request(db, owner_id="alice")
    await _insert_request(db, owner_id="bob")
    await _insert_request(db, owner_id="alice")

    result = await db.get_system_overview()
    assert result["total_users"] == 2
    assert result["total_requests_24h"] == 3
    assert result["active_users_24h"] == 2
    assert result["avg_latency_ms"] > 0


# ---------------------------------------------------------------------------
# prune_old_requests
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_prune_old_requests_deletes_old_rows(db: StatsDB):
    """prune_old_requests removes rows older than N days."""
    # Insert an artificially old row
    old_ts = (date.today() - timedelta(days=60)).strftime("%Y-%m-%dT00:00:00.000Z")
    await db._conn.execute(
        """
        INSERT INTO request_log (owner_id, endpoint, method, status_code, latency_ms, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
        """,
        ("alice", "/old", "GET", 200, 10.0, old_ts),
    )
    await db._conn.commit()

    # Insert a fresh row
    await _insert_request(db, owner_id="alice")

    deleted = await db.prune_old_requests(days=30)
    assert deleted == 1

    async with db._conn.execute("SELECT COUNT(*) AS cnt FROM request_log") as cursor:
        row = await cursor.fetchone()
    assert row["cnt"] == 1  # only the fresh row remains


@pytest.mark.asyncio
async def test_prune_old_requests_returns_zero_when_nothing_to_prune(db: StatsDB):
    """prune_old_requests returns 0 when all rows are recent."""
    await _insert_request(db)
    deleted = await db.prune_old_requests(days=30)
    assert deleted == 0


# ---------------------------------------------------------------------------
# Concurrent writes (WAL mode)
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_concurrent_writes_wal_mode(tmp_path):
    """Concurrent record_request calls succeed without SQLITE_BUSY under WAL."""
    db_path = str(tmp_path / "concurrent_test.db")
    stats = StatsDB(db_path=db_path)
    await stats.initialize()

    tasks = [
        _insert_request(stats, owner_id=f"user{i}", latency_ms=float(i))
        for i in range(50)
    ]
    await asyncio.gather(*tasks)

    async with stats._conn.execute("SELECT COUNT(*) AS cnt FROM request_log") as cursor:
        row = await cursor.fetchone()
    assert row["cnt"] == 50

    await stats.close()


# ---------------------------------------------------------------------------
# Config field parsing
# ---------------------------------------------------------------------------

def test_config_stats_defaults():
    """CortexConfig has correct default values for stats fields."""
    from cortex.config import CortexConfig
    cfg = CortexConfig()
    assert cfg.stats_enabled is True
    assert cfg.stats_db_path == "/data/stats.db"
    assert cfg.stats_prune_after_days == 30


def test_config_stats_from_toml(tmp_path):
    """CortexConfig parses [cortex.stats] section from TOML."""
    toml_content = """
[cortex.stats]
enabled = false
db_path = "/tmp/custom_stats.db"
prune_after_days = 7
"""
    toml_file = tmp_path / "test_config.toml"
    toml_file.write_text(toml_content)

    from cortex.config import CortexConfig
    cfg = CortexConfig.from_toml(str(toml_file))
    assert cfg.stats_enabled is False
    assert cfg.stats_db_path == "/tmp/custom_stats.db"
    assert cfg.stats_prune_after_days == 7
