"""Tests for audit round 2 hardening fixes.

Covers: Content-Length validation, bounded caches, SQLite indexes,
proxy metrics bounds, stuck job cleanup, log file parsing safety.
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

# ── Content-Length validation ────────────────────────────────────────────────


def test_max_body_size_invalid_content_length_no_crash():
    """MaxBodySizeMiddleware handles invalid Content-Length without crashing."""
    import asyncio

    from agent_bom.api.server import MaxBodySizeMiddleware

    middleware = MaxBodySizeMiddleware(app=MagicMock())

    # Simulate request with invalid Content-Length
    request = MagicMock()
    request.headers = {"content-length": "not-a-number"}
    call_next = MagicMock()

    resp = asyncio.run(middleware.dispatch(request, call_next))
    assert resp.status_code == 400


def test_max_body_size_overflow_no_crash():
    """MaxBodySizeMiddleware handles overflowing Content-Length."""
    import asyncio

    from agent_bom.api.server import MaxBodySizeMiddleware

    middleware = MaxBodySizeMiddleware(app=MagicMock())

    request = MagicMock()
    request.headers = {"content-length": "999999999999999999999999"}
    call_next = MagicMock()

    resp = asyncio.run(middleware.dispatch(request, call_next))
    # Should be 400 (overflow) or 413 (too large)
    assert resp.status_code in (400, 413)


# ── Bounded transitive caches ────────────────────────────────────────────────


def test_cache_put_bounded():
    """_cache_put evicts oldest entries when cache exceeds limit."""
    from agent_bom.transitive import _cache_put

    cache: dict[str, dict] = {}
    for i in range(100):
        _cache_put(cache, f"key-{i}", {"data": i})

    # Should stay bounded (5000 limit, but only 100 entries)
    assert len(cache) == 100


def test_cache_put_eviction():
    """_cache_put evicts entries when exceeding max."""
    from agent_bom import transitive

    original = transitive._MAX_TRANSITIVE_CACHE
    try:
        transitive._MAX_TRANSITIVE_CACHE = 10
        cache: dict[str, dict] = {}
        for i in range(20):
            transitive._cache_put(cache, f"key-{i}", {"data": i})
        assert len(cache) <= 10
        # Most recent entries should remain
        assert "key-19" in cache
    finally:
        transitive._MAX_TRANSITIVE_CACHE = original


def test_max_transitive_cache_defined():
    """_MAX_TRANSITIVE_CACHE constant is set."""
    from agent_bom.transitive import _MAX_TRANSITIVE_CACHE

    assert _MAX_TRANSITIVE_CACHE == 5_000


# ── Bounded AI cache ─────────────────────────────────────────────────────────


def test_ai_cache_put_bounded():
    """_ai_cache_put evicts entries when exceeding limit."""
    from agent_bom import ai_enrich

    original = ai_enrich._MAX_AI_CACHE
    try:
        ai_enrich._MAX_AI_CACHE = 5
        ai_enrich._cache.clear()
        for i in range(10):
            ai_enrich._ai_cache_put(f"key-{i}", f"value-{i}")
        assert len(ai_enrich._cache) <= 5
        assert "key-9" in ai_enrich._cache
    finally:
        ai_enrich._MAX_AI_CACHE = original
        ai_enrich._cache.clear()


# ── Proxy metrics bounds ─────────────────────────────────────────────────────


def test_proxy_latencies_bounded():
    """ProxyMetrics.latencies_ms doesn't grow unboundedly."""
    from agent_bom.proxy import ProxyMetrics

    m = ProxyMetrics()
    for i in range(15_000):
        m.record_latency(float(i))
    # Should have been trimmed
    assert len(m.latencies_ms) <= ProxyMetrics._MAX_LATENCY_ENTRIES


# ── SQLite indexes ───────────────────────────────────────────────────────────


def test_sqlite_job_store_has_indexes():
    """SQLiteJobStore creates status and completed_at indexes."""
    import sqlite3
    import tempfile

    from agent_bom.api.store import SQLiteJobStore

    with tempfile.NamedTemporaryFile(suffix=".db") as tmp:
        SQLiteJobStore(tmp.name)
        conn = sqlite3.connect(tmp.name)
        indexes = [r[1] for r in conn.execute("PRAGMA index_list('jobs')").fetchall()]
        assert "idx_jobs_status" in indexes
        assert "idx_jobs_completed" in indexes
        conn.close()


def test_sqlite_schedule_store_has_index():
    """SQLiteScheduleStore creates enabled+next_run index."""
    import sqlite3
    import tempfile

    from agent_bom.api.schedule_store import SQLiteScheduleStore

    with tempfile.NamedTemporaryFile(suffix=".db") as tmp:
        SQLiteScheduleStore(tmp.name)
        conn = sqlite3.connect(tmp.name)
        indexes = [r[1] for r in conn.execute("PRAGMA index_list('schedules')").fetchall()]
        assert "idx_sched_due" in indexes
        conn.close()


def test_sqlite_policy_store_has_timestamp_index():
    """SQLitePolicyStore creates timestamp index on audit log."""
    import sqlite3
    import tempfile

    from agent_bom.api.policy_store import SQLitePolicyStore

    with tempfile.NamedTemporaryFile(suffix=".db") as tmp:
        SQLitePolicyStore(tmp.name)
        conn = sqlite3.connect(tmp.name)
        indexes = [r[1] for r in conn.execute("PRAGMA index_list('policy_audit_log')").fetchall()]
        assert "idx_pal_ts" in indexes
        conn.close()


# ── Registry JSON parse safety ───────────────────────────────────────────────


def test_load_registry_handles_corrupt_json():
    """_load_registry returns empty list on corrupt JSON."""
    from agent_bom.api.server import _load_registry

    # Clear LRU cache to force reload
    _load_registry.cache_clear()

    with patch("agent_bom.api.routes.connectors._Path") as mock_path_cls:
        mock_path = MagicMock()
        mock_path.exists.return_value = True
        mock_path.read_text.return_value = "NOT VALID JSON {{"
        mock_path_cls.return_value.__truediv__ = MagicMock(return_value=mock_path)
        # The function should return [] not crash
        # Note: _load_registry uses lru_cache, so we test via the error path
    _load_registry.cache_clear()


# ── Stuck job cleanup ────────────────────────────────────────────────────────


def test_stuck_job_timeout_defined():
    """_STUCK_JOB_TIMEOUT is set to a reasonable value."""
    from agent_bom.api.server import _STUCK_JOB_TIMEOUT

    assert _STUCK_JOB_TIMEOUT == 1800  # 30 minutes


# ── Log file parsing safety ──────────────────────────────────────────────────


def test_read_alerts_handles_missing_file():
    """_read_alerts_from_log returns empty on missing file."""
    from pathlib import Path

    from agent_bom.api.routes.proxy import _read_alerts_from_log

    result = _read_alerts_from_log(Path("/nonexistent/file.jsonl"))
    assert result == []


def test_read_metrics_handles_missing_file():
    """_read_metrics_from_log returns None on missing file."""
    from pathlib import Path

    from agent_bom.api.routes.proxy import _read_metrics_from_log

    result = _read_metrics_from_log(Path("/nonexistent/file.jsonl"))
    assert result is None


def test_read_alerts_from_log_with_valid_data():
    """_read_alerts_from_log correctly parses valid JSONL."""
    import json
    import tempfile
    from pathlib import Path

    from agent_bom.api.routes.proxy import _read_alerts_from_log

    content = "\n".join(
        [
            json.dumps({"type": "runtime_alert", "message": "test alert"}),
            json.dumps({"type": "proxy_summary", "data": "ignore"}),
            json.dumps({"type": "runtime_alert", "message": "second alert"}),
            "invalid json line",
        ]
    )

    with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f:
        f.write(content)
        f.flush()
        result = _read_alerts_from_log(Path(f.name))

    assert len(result) == 2
    assert result[0]["message"] == "test alert"


# ── ThreadPoolExecutor sizing ────────────────────────────────────────────────


def test_executor_has_reasonable_workers():
    """ThreadPoolExecutor has at least 4 workers."""
    from agent_bom.api.server import _executor

    assert _executor._max_workers >= 4


# ── Enrichment cache logging ─────────────────────────────────────────────────


def test_enrichment_cache_load_failure_logged(caplog):
    """Enrichment cache load failure is logged, not silently swallowed."""
    import logging

    from agent_bom import enrichment

    # Reset state
    enrichment._enrichment_cache_loaded = False

    with (
        caplog.at_level(logging.DEBUG, logger="agent_bom.enrichment"),
        patch.object(enrichment._ENRICHMENT_CACHE_DIR.__class__, "__truediv__", side_effect=OSError("disk error")),
    ):
        try:
            enrichment._load_enrichment_cache()
        except Exception:
            pass

    # Restore
    enrichment._enrichment_cache_loaded = False


# ── Policy audit log retention ───────────────────────────────────────────────


def test_policy_audit_log_cleanup():
    """SQLitePolicyStore.cleanup_audit_log removes oldest entries."""
    import tempfile

    from agent_bom.api.policy_store import PolicyAuditEntry, SQLitePolicyStore

    with tempfile.NamedTemporaryFile(suffix=".db") as tmp:
        store = SQLitePolicyStore(tmp.name)
        # Insert 20 audit entries
        for i in range(20):
            entry = PolicyAuditEntry(
                entry_id=f"entry-{i}",
                policy_id="pol-1",
                policy_name="test-policy",
                rule_id="rule-1",
                agent_name="agent-1",
                tool_name="tool-1",
                action_taken="allow",
                reason="test",
                timestamp=f"2025-01-01T00:{i:02d}:00Z",
            )
            store.put_audit_entry(entry)

        # Cleanup to max 10
        removed = store.cleanup_audit_log(max_entries=10)
        assert removed == 10

        # Should have 10 remaining
        remaining = store.list_audit_entries(limit=100)
        assert len(remaining) == 10
