"""Tests for ReputationEngine — weighted scoring from ProbeStore data (issue #290)."""

from __future__ import annotations

import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path

import pytest

from llmhosts.marketplace.probes import ProbeResult, ProbeStore
from llmhosts.marketplace.reputation import (
    REPUTATION_TIER_ELITE,
    REPUTATION_TIER_STANDARD,
    REPUTATION_TIER_UNVERIFIED,
    REPUTATION_TIER_VERIFIED,
    TIER_MIN_SCORES,
    DeviceScore,
    ReputationEngine,
    min_score_for_tier,
    reputation_tier,
)


def _now() -> datetime:
    return datetime.now(timezone.utc)


async def _make_store(tmp: str) -> ProbeStore:
    """Create and initialise a ProbeStore in a temp dir."""
    store = ProbeStore(Path(tmp) / "probes.db")
    await store.initialize()
    return store


async def _seed_probes(
    store: ProbeStore,
    device_id: str,
    successes: int,
    failures: int,
    latency_ms: float = 50.0,
) -> None:
    """Seed *successes* successful and *failures* failed probes, newest first."""
    now = _now()
    # Insert failures first (older), then successes (newer) so streak = successes
    for i in range(failures):
        await store.append(
            ProbeResult(
                device_id=device_id,
                timestamp=now - timedelta(hours=successes + failures - i),
                latency_ms=latency_ms,
                success=False,
            )
        )
    for i in range(successes):
        await store.append(
            ProbeResult(
                device_id=device_id,
                timestamp=now - timedelta(hours=successes - i - 1),
                latency_ms=latency_ms,
                success=True,
            )
        )


# ---------------------------------------------------------------------------
# Basic score computation
# ---------------------------------------------------------------------------


@pytest.mark.unit
async def test_perfect_record_high_score() -> None:
    """Device with 100% uptime, low latency, 100% success, long streak => high score."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await _seed_probes(store, "d1", successes=100, failures=0, latency_ms=20.0)
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("d1")
        assert isinstance(result, DeviceScore)
        assert result.score >= 90
        assert result.tier in (REPUTATION_TIER_VERIFIED, REPUTATION_TIER_ELITE)
        assert result.uptime_pct == 100.0
        assert result.consecutive_successes == 100
        await store.close()


@pytest.mark.unit
async def test_all_failures_low_score() -> None:
    """Device with 0% success rate and 0 streak => low score."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await _seed_probes(store, "d1", successes=0, failures=50, latency_ms=100.0)
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("d1")
        assert result.score < 10
        assert result.tier == REPUTATION_TIER_UNVERIFIED
        assert result.uptime_pct == 0.0
        assert result.success_rate == 0.0
        assert result.consecutive_successes == 0
        await store.close()


@pytest.mark.unit
async def test_mixed_record_moderate_score() -> None:
    """Device with ~66% success rate gets a moderate score."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await _seed_probes(store, "d1", successes=20, failures=10, latency_ms=200.0)
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("d1")
        assert 40 <= result.score <= 80
        assert result.tier in (REPUTATION_TIER_UNVERIFIED, REPUTATION_TIER_STANDARD)
        await store.close()


@pytest.mark.unit
async def test_score_clamped_0_100() -> None:
    """Score is always in [0, 100]."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await _seed_probes(store, "d1", successes=50, failures=0, latency_ms=10.0)
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("d1")
        assert 0 <= result.score <= 100
        await store.close()


# ---------------------------------------------------------------------------
# Component weights
# ---------------------------------------------------------------------------


@pytest.mark.unit
async def test_low_latency_better_than_high() -> None:
    """Lower average latency should produce a higher score, all else equal."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await _seed_probes(store, "fast", successes=50, failures=0, latency_ms=20.0)
        await _seed_probes(store, "slow", successes=50, failures=0, latency_ms=3000.0)
        engine = ReputationEngine(probe_store=store)
        fast_result = await engine.compute_score("fast")
        slow_result = await engine.compute_score("slow")
        assert fast_result.score > slow_result.score
        await store.close()


@pytest.mark.unit
async def test_higher_uptime_better_score() -> None:
    """Higher uptime percentage should produce a higher score."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await _seed_probes(store, "reliable", successes=95, failures=5, latency_ms=50.0)
        await _seed_probes(store, "flaky", successes=50, failures=50, latency_ms=50.0)
        engine = ReputationEngine(probe_store=store)
        reliable = await engine.compute_score("reliable")
        flaky = await engine.compute_score("flaky")
        assert reliable.score > flaky.score
        await store.close()


@pytest.mark.unit
async def test_streak_bonus() -> None:
    """Longer consecutive success streak should boost score."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        # Device with long streak: all successes after failures
        await _seed_probes(store, "streaky", successes=80, failures=5, latency_ms=50.0)
        # Device with broken streak: interleave failures
        store2_path = Path(tmp) / "probes2.db"
        store2 = ProbeStore(store2_path)
        await store2.initialize()
        now = _now()
        for i in range(85):
            success = (i % 10) != 0  # fail every 10th probe
            await store2.append(
                ProbeResult(
                    device_id="broken", timestamp=now - timedelta(hours=85 - i), latency_ms=50.0, success=success
                )
            )

        engine1 = ReputationEngine(probe_store=store)
        engine2 = ReputationEngine(probe_store=store2)
        streaky = await engine1.compute_score("streaky")
        broken = await engine2.compute_score("broken")
        assert streaky.consecutive_successes > broken.consecutive_successes
        await store.close()
        await store2.close()


# ---------------------------------------------------------------------------
# Tier assignment
# ---------------------------------------------------------------------------


@pytest.mark.unit
async def test_tier_elite() -> None:
    """Score >= 95 => Elite."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await _seed_probes(store, "d1", successes=200, failures=0, latency_ms=10.0)
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("d1")
        assert result.tier == REPUTATION_TIER_ELITE
        await store.close()


@pytest.mark.unit
async def test_tier_standard() -> None:
    """Score in [50, 79] => Standard."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        # ~70% uptime, moderate latency
        await _seed_probes(store, "d1", successes=70, failures=30, latency_ms=300.0)
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("d1")
        assert result.tier in (REPUTATION_TIER_STANDARD, REPUTATION_TIER_VERIFIED)
        await store.close()


@pytest.mark.unit
async def test_get_tier_static() -> None:
    """ReputationEngine.get_tier is a static wrapper around reputation_tier."""
    assert ReputationEngine.get_tier(99) == REPUTATION_TIER_ELITE
    assert ReputationEngine.get_tier(85) == REPUTATION_TIER_VERIFIED
    assert ReputationEngine.get_tier(60) == REPUTATION_TIER_STANDARD
    assert ReputationEngine.get_tier(30) == REPUTATION_TIER_UNVERIFIED


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


@pytest.mark.unit
async def test_no_probe_data() -> None:
    """Device with no probe data at all => score 0, Unverified."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("nonexistent")
        assert result.score == 0
        assert result.tier == REPUTATION_TIER_UNVERIFIED
        assert result.uptime_pct == 0.0
        assert result.avg_latency_ms == 0.0
        assert result.success_rate == 0.0
        assert result.consecutive_successes == 0
        await store.close()


@pytest.mark.unit
async def test_single_probe_success() -> None:
    """Device with a single successful probe."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await store.append(ProbeResult(device_id="d1", timestamp=_now(), latency_ms=30.0, success=True))
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("d1")
        assert result.score > 0
        assert result.uptime_pct == 100.0
        assert result.consecutive_successes == 1
        await store.close()


@pytest.mark.unit
async def test_single_probe_failure() -> None:
    """Device with a single failed probe."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await store.append(ProbeResult(device_id="d1", timestamp=_now(), latency_ms=100.0, success=False))
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("d1")
        assert result.score == 0
        assert result.uptime_pct == 0.0
        assert result.consecutive_successes == 0
        await store.close()


# ---------------------------------------------------------------------------
# Batch computation
# ---------------------------------------------------------------------------


@pytest.mark.unit
async def test_get_scores_batch() -> None:
    """get_scores returns a dict of DeviceScore for multiple devices."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await _seed_probes(store, "a", successes=50, failures=0, latency_ms=30.0)
        await _seed_probes(store, "b", successes=20, failures=30, latency_ms=200.0)
        await _seed_probes(store, "c", successes=0, failures=10, latency_ms=500.0)
        engine = ReputationEngine(probe_store=store)
        scores = await engine.get_scores(["a", "b", "c"])
        assert len(scores) == 3
        assert scores["a"].score > scores["b"].score > scores["c"].score
        assert all(isinstance(v, DeviceScore) for v in scores.values())
        await store.close()


@pytest.mark.unit
async def test_get_scores_empty_list() -> None:
    """get_scores with empty list returns empty dict."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        engine = ReputationEngine(probe_store=store)
        scores = await engine.get_scores([])
        assert scores == {}
        await store.close()


@pytest.mark.unit
async def test_get_scores_missing_device() -> None:
    """get_scores for device with no data returns score 0."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        engine = ReputationEngine(probe_store=store)
        scores = await engine.get_scores(["missing"])
        assert scores["missing"].score == 0
        await store.close()


# ---------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_min_score_for_tier() -> None:
    """min_score_for_tier returns correct thresholds."""
    assert min_score_for_tier("Unverified") == 0
    assert min_score_for_tier("Standard") == 50
    assert min_score_for_tier("Verified") == 80
    assert min_score_for_tier("Elite") == 95
    assert min_score_for_tier("Unknown") == 0


@pytest.mark.unit
def test_tier_min_scores_dict() -> None:
    """TIER_MIN_SCORES has all four tiers."""
    assert len(TIER_MIN_SCORES) == 4
    assert REPUTATION_TIER_UNVERIFIED in TIER_MIN_SCORES
    assert REPUTATION_TIER_ELITE in TIER_MIN_SCORES


@pytest.mark.unit
def test_reputation_tier_boundaries() -> None:
    """reputation_tier handles exact boundary values correctly."""
    assert reputation_tier(0) == REPUTATION_TIER_UNVERIFIED
    assert reputation_tier(49) == REPUTATION_TIER_UNVERIFIED
    assert reputation_tier(50) == REPUTATION_TIER_STANDARD
    assert reputation_tier(79) == REPUTATION_TIER_STANDARD
    assert reputation_tier(80) == REPUTATION_TIER_VERIFIED
    assert reputation_tier(94) == REPUTATION_TIER_VERIFIED
    assert reputation_tier(95) == REPUTATION_TIER_ELITE
    assert reputation_tier(100) == REPUTATION_TIER_ELITE


# ---------------------------------------------------------------------------
# DeviceScore dataclass
# ---------------------------------------------------------------------------


@pytest.mark.unit
async def test_device_score_fields() -> None:
    """DeviceScore has all required fields populated."""
    with tempfile.TemporaryDirectory() as tmp:
        store = await _make_store(tmp)
        await _seed_probes(store, "d1", successes=30, failures=10, latency_ms=150.0)
        engine = ReputationEngine(probe_store=store)
        result = await engine.compute_score("d1")
        assert result.device_id == "d1"
        assert isinstance(result.score, int)
        assert isinstance(result.tier, str)
        assert isinstance(result.uptime_pct, float)
        assert isinstance(result.avg_latency_ms, float)
        assert isinstance(result.success_rate, float)
        assert isinstance(result.consecutive_successes, int)
        await store.close()
