import time

import pytest
from meshy.path_history import PathHistoryService, PathRecord
from meshy.storage import Storage

CONTACT_HEX = "cc" * 32


def _make_storage():
    return Storage(db_path=":memory:")


_SENTINEL = object()


def _make_record(success=10, failure=0, trip_ms=500, last_success_at=_SENTINEL, **kwargs):
    return PathRecord(
        id=1,
        contact_key_hex=CONTACT_HEX,
        path_bytes="aabb",
        path_hash_size=1,
        hop_count=2,
        success_count=success,
        failure_count=failure,
        last_trip_ms=trip_ms,
        route_weight=2.5,
        last_success_at=time.time() if last_success_at is _SENTINEL else last_success_at,
        **kwargs,
    )


# ─── score() ──────────────────────────────────────────────────


class TestScore:
    def setup_method(self):
        self.storage = _make_storage()
        self.service = PathHistoryService(self.storage)

    def teardown_method(self):
        self.storage.close()

    def test_reliability_dominates(self):
        good = _make_record(success=100, failure=0)
        bad = _make_record(success=0, failure=100)
        assert self.service.score(good) > self.service.score(bad)

    def test_faster_path_scores_higher(self):
        fast = _make_record(success=10, failure=0, trip_ms=100)
        slow = _make_record(success=10, failure=0, trip_ms=10000)
        assert self.service.score(fast) > self.service.score(slow)

    def test_fresher_path_scores_higher(self):
        now = time.time()
        fresh = _make_record(last_success_at=now)
        stale = _make_record(last_success_at=now - 30 * 86400)
        assert self.service.score(fresh) > self.service.score(stale)

    def test_no_trip_ms_uses_default(self):
        now = time.time()
        rec_none = _make_record(trip_ms=None, last_success_at=now)
        rec_zero = _make_record(trip_ms=0, last_success_at=now)
        rec_fast = _make_record(trip_ms=100, last_success_at=now)
        score_none = self.service.score(rec_none)
        score_zero = self.service.score(rec_zero)
        score_fast = self.service.score(rec_fast)
        assert score_none == pytest.approx(score_zero)
        assert score_fast > score_none

    def test_no_last_success_zero_freshness(self):
        rec = _make_record(last_success_at=None)
        rec_fresh = _make_record()
        score_none = self.service.score(rec)
        score_fresh = self.service.score(rec_fresh)
        assert score_none < score_fresh
        reliability = (10 + 1) / (10 + 2) * 0.6
        latency = 1.0 / (1.0 + 500 / 10000.0) * 0.2
        expected = reliability + latency + 0.0
        assert abs(score_none - expected) < 0.001


# ─── select_path() ────────────────────────────────────────────


class TestSelectPath:
    def setup_method(self):
        self.storage = _make_storage()
        self.service = PathHistoryService(self.storage)

    def teardown_method(self):
        self.storage.close()

    def test_empty_returns_none(self):
        assert self.service.select_path(CONTACT_HEX) is None

    def test_returns_best_scored(self):
        self.storage.upsert_path_record(CONTACT_HEX, "good", 1, 1, success=True, trip_ms=100)
        self.storage.upsert_path_record(CONTACT_HEX, "bad", 1, 3, success=False)
        # Add more successes to "good"
        for _ in range(5):
            self.storage.upsert_path_record(CONTACT_HEX, "good", 1, 1, success=True, trip_ms=100)
        best = self.service.select_path(CONTACT_HEX)
        assert best is not None
        assert best.path_bytes == "good"

    def test_exclude_paths(self):
        self.storage.upsert_path_record(CONTACT_HEX, "path_a", 1, 1, success=True, trip_ms=100)
        self.storage.upsert_path_record(CONTACT_HEX, "path_b", 1, 2, success=True, trip_ms=200)
        result = self.service.select_path(CONTACT_HEX, exclude_paths=["path_a"])
        assert result is not None
        assert result.path_bytes == "path_b"

    def test_exclude_all_returns_none(self):
        self.storage.upsert_path_record(CONTACT_HEX, "only", 1, 1, success=True)
        assert self.service.select_path(CONTACT_HEX, exclude_paths=["only"]) is None


# ─── record_result() ──────────────────────────────────────────


class TestRecordResult:
    def setup_method(self):
        self.storage = _make_storage()
        self.service = PathHistoryService(self.storage)

    def teardown_method(self):
        self.storage.close()

    def test_success_increments(self):
        self.service.record_result(CONTACT_HEX, "path1", 1, 2, success=True, trip_ms=300)
        self.service.record_result(CONTACT_HEX, "path1", 1, 2, success=True, trip_ms=250)
        history = self.storage.get_path_history(CONTACT_HEX)
        assert history[0]["success_count"] == 2

    def test_failure_increments(self):
        self.service.record_result(CONTACT_HEX, "path1", 1, 2, success=True)
        self.service.record_result(CONTACT_HEX, "path1", 1, 2, success=False)
        history = self.storage.get_path_history(CONTACT_HEX)
        assert history[0]["failure_count"] == 1

    def test_weight_zero_deletes_record(self):
        self.service.record_result(CONTACT_HEX, "path1", 1, 2, success=True)
        for _ in range(20):
            self.service.record_result(CONTACT_HEX, "path1", 1, 2, success=False)
        assert self.storage.get_path_history(CONTACT_HEX) == []
        assert not self.service.has_paths(CONTACT_HEX)

    def test_weight_zero_deletes_only_dead_path(self):
        self.service.record_result(CONTACT_HEX, "path1", 1, 2, success=True)
        self.service.record_result(CONTACT_HEX, "path2", 1, 1, success=True, trip_ms=200)
        for _ in range(20):
            self.service.record_result(CONTACT_HEX, "path1", 1, 2, success=False)
        history = self.storage.get_path_history(CONTACT_HEX)
        assert len(history) == 1
        assert history[0]["path_bytes"] == "path2"
