"""Tests for myburn.trust — payload integrity checks, hash chains, scoring."""
from __future__ import annotations

import os
import tempfile
import unittest

# Set up temp dirs before importing anything
_tmpdir = tempfile.mkdtemp()
os.environ.setdefault("MYBURN_CLOUD_DB", os.path.join(_tmpdir, "cloud.db"))
os.environ.setdefault("MYBURN_DATA_DIR", _tmpdir)

from myburn.trust import (
    TrustFlag,
    compute_payload_hash,
    check_model_names,
    check_price_consistency,
    check_daily_sum,
    check_token_consistency,
    check_active_days,
    check_benford,
    verify_hash_chain,
    compute_trust_score,
    validate_trust,
)


def _make_clean_payload(**overrides) -> dict:
    """Create a payload that passes all trust checks.

    Cost/token ratios are realistic:
    - opus: 10M tokens @ $13/MTok (between cache_read=$0.50 and output=$25)
    - haiku: 5M tokens @ $1/MTok (between cache_read=$0.10 and output=$5)
    """
    payload = {
        "version": "1.0",
        "cliVersion": "0.1.0",
        "month": "2026-02",
        "generatedAt": "2026-02-25T12:00:00Z",
        "cost": 135.0,
        "tokens": {
            "total": 15000000,
            "input": 1000000,
            "output": 500000,
            "cacheRead": 13000000,
            "cacheCreation": 500000,
        },
        "sessions": 3,
        "activeDays": 3,
        "streak": 3,
        "peakDay": {"date": "2026-02-02", "cost": 55.0},
        "models": [
            {"name": "claude-opus-4-6", "tokens": 10000000, "cost": 130.0},
            {"name": "claude-haiku-4-5-20251001", "tokens": 5000000, "cost": 5.0},
        ],
        "days": [
            {"date": "2026-02-01", "cost": 50.0},
            {"date": "2026-02-02", "cost": 55.0},
            {"date": "2026-02-03", "cost": 30.0},
        ],
        "turns": {"total": 10, "yolo": 2, "yoloPct": 20.0},
        "tools": {"Read": 4, "Bash": 3},
    }
    payload.update(overrides)
    return payload


# ---------------------------------------------------------------------------
# compute_payload_hash
# ---------------------------------------------------------------------------
class TestComputePayloadHash(unittest.TestCase):
    def test_deterministic(self):
        p = _make_clean_payload()
        h1 = compute_payload_hash(p)
        h2 = compute_payload_hash(p)
        self.assertEqual(h1, h2)

    def test_starts_with_sha256(self):
        h = compute_payload_hash(_make_clean_payload())
        self.assertTrue(h.startswith("sha256:"))

    def test_meta_fields_excluded(self):
        p1 = _make_clean_payload()
        p2 = _make_clean_payload()
        p2["_prevHash"] = "sha256:abc123"
        p2["_synced"] = True
        self.assertEqual(compute_payload_hash(p1), compute_payload_hash(p2))

    def test_different_payloads_differ(self):
        p1 = _make_clean_payload(cost=100.0)
        p2 = _make_clean_payload(cost=200.0)
        self.assertNotEqual(compute_payload_hash(p1), compute_payload_hash(p2))

    def test_key_order_irrelevant(self):
        p1 = {"a": 1, "b": 2}
        p2 = {"b": 2, "a": 1}
        self.assertEqual(compute_payload_hash(p1), compute_payload_hash(p2))


# ---------------------------------------------------------------------------
# check_model_names
# ---------------------------------------------------------------------------
class TestCheckModelNames(unittest.TestCase):
    def test_valid_models_pass(self):
        p = _make_clean_payload()
        flags = check_model_names(p)
        self.assertEqual(flags, [])

    def test_gpt4_hard_rejects(self):
        p = _make_clean_payload()
        p["models"] = [{"name": "gpt-4-turbo", "tokens": 1000, "cost": 10.0}]
        flags = check_model_names(p)
        self.assertEqual(len(flags), 1)
        self.assertEqual(flags[0].code, "UNKNOWN_MODEL")
        self.assertEqual(flags[0].severity, "hard")

    def test_empty_models_ok(self):
        p = _make_clean_payload()
        p["models"] = []
        flags = check_model_names(p)
        self.assertEqual(flags, [])

    def test_no_models_key_ok(self):
        p = _make_clean_payload()
        del p["models"]
        flags = check_model_names(p)
        self.assertEqual(flags, [])

    def test_mixed_valid_invalid(self):
        p = _make_clean_payload()
        p["models"] = [
            {"name": "claude-sonnet-4-6", "tokens": 1000, "cost": 10.0},
            {"name": "llama-3-70b", "tokens": 500, "cost": 5.0},
        ]
        flags = check_model_names(p)
        self.assertEqual(len(flags), 1)
        self.assertIn("llama", flags[0].message)

    def test_all_prefixes_valid(self):
        p = _make_clean_payload()
        p["models"] = [
            {"name": "claude-opus-4-6", "tokens": 1000, "cost": 10.0},
            {"name": "claude-sonnet-4-5-20250514", "tokens": 1000, "cost": 10.0},
            {"name": "claude-haiku-4-5-20251001", "tokens": 1000, "cost": 10.0},
        ]
        flags = check_model_names(p)
        self.assertEqual(flags, [])


# ---------------------------------------------------------------------------
# check_price_consistency
# ---------------------------------------------------------------------------
class TestCheckPriceConsistency(unittest.TestCase):
    def test_correct_pricing_passes(self):
        # Opus 4.6: cache_read=$0.50/MTok, output=$25/MTok
        # 10M tokens @ $13/MTok is between $0.50 and $25 — should pass
        p = _make_clean_payload()
        p["models"] = [{"name": "claude-opus-4-6", "tokens": 10000000, "cost": 130.0}]
        flags = check_price_consistency(p)
        price_flags = [f for f in flags if f.code in ("PRICE_TOO_LOW", "PRICE_TOO_HIGH")]
        self.assertEqual(price_flags, [])

    def test_impossibly_cheap_flagged(self):
        # Rate below cache_read * 0.8
        p = _make_clean_payload()
        p["models"] = [{"name": "claude-opus-4-6", "tokens": 1000000, "cost": 0.01}]
        flags = check_price_consistency(p)
        low_flags = [f for f in flags if f.code == "PRICE_TOO_LOW"]
        self.assertGreater(len(low_flags), 0)

    def test_impossibly_expensive_flagged(self):
        # Rate above output * 1.2 = $25 * 1.2 = $30/MTok
        p = _make_clean_payload()
        p["models"] = [{"name": "claude-opus-4-6", "tokens": 100000, "cost": 5000.0}]
        flags = check_price_consistency(p)
        high_flags = [f for f in flags if f.code == "PRICE_TOO_HIGH"]
        self.assertGreater(len(high_flags), 0)

    def test_cost_sum_mismatch_flagged(self):
        p = _make_clean_payload()
        p["cost"] = 1000.0
        p["models"] = [
            {"name": "claude-opus-4-6", "tokens": 100000, "cost": 100.0},
        ]
        flags = check_price_consistency(p)
        sum_flags = [f for f in flags if f.code == "COST_SUM_MISMATCH"]
        self.assertGreater(len(sum_flags), 0)

    def test_zero_tokens_skipped(self):
        p = _make_clean_payload()
        p["models"] = [{"name": "claude-opus-4-6", "tokens": 0, "cost": 0}]
        flags = check_price_consistency(p)
        self.assertEqual(flags, [])

    def test_unknown_model_skipped(self):
        # Unknown model pricing is not checked (no PRICING entry)
        p = _make_clean_payload()
        p["models"] = [{"name": "claude-sonnet-99", "tokens": 1000, "cost": 10.0}]
        flags = check_price_consistency(p)
        # No price flags for unknown model (model check is separate)
        price_flags = [f for f in flags if f.code in ("PRICE_TOO_LOW", "PRICE_TOO_HIGH")]
        self.assertEqual(price_flags, [])


# ---------------------------------------------------------------------------
# check_daily_sum
# ---------------------------------------------------------------------------
class TestCheckDailySum(unittest.TestCase):
    def test_matching_passes(self):
        p = _make_clean_payload()
        # days sum to 50+55+30 = 135, total = 135
        flags = check_daily_sum(p)
        self.assertEqual(flags, [])

    def test_mismatch_hard_rejects(self):
        p = _make_clean_payload()
        p["cost"] = 135.0
        p["days"] = [{"date": "2026-02-01", "cost": 10.0}]  # sum=10, total=135 → 93% off
        flags = check_daily_sum(p)
        self.assertEqual(len(flags), 1)
        self.assertEqual(flags[0].code, "DAILY_SUM_MISMATCH")
        self.assertEqual(flags[0].severity, "hard")

    def test_within_tolerance_passes(self):
        p = _make_clean_payload()
        p["cost"] = 100.0
        # 15% off is within 20% tolerance
        p["days"] = [{"date": "2026-02-01", "cost": 85.0}]
        flags = check_daily_sum(p)
        self.assertEqual(flags, [])

    def test_no_days_skipped(self):
        p = _make_clean_payload()
        p["days"] = []
        flags = check_daily_sum(p)
        self.assertEqual(flags, [])

    def test_zero_cost_skipped(self):
        p = _make_clean_payload(cost=0)
        flags = check_daily_sum(p)
        self.assertEqual(flags, [])


# ---------------------------------------------------------------------------
# check_token_consistency
# ---------------------------------------------------------------------------
class TestCheckTokenConsistency(unittest.TestCase):
    def test_exact_sum_passes(self):
        p = _make_clean_payload()
        # 5000 + 1800 + 160000 + 700 = 167500 ✓
        flags = check_token_consistency(p)
        self.assertEqual(flags, [])

    def test_sum_mismatch_hard_rejects(self):
        p = _make_clean_payload()
        p["tokens"]["total"] = 999999  # wrong
        flags = check_token_consistency(p)
        hard = [f for f in flags if f.code == "TOKEN_SUM_MISMATCH"]
        self.assertEqual(len(hard), 1)
        self.assertEqual(hard[0].severity, "hard")

    def test_model_token_mismatch_soft(self):
        p = _make_clean_payload()
        # Model tokens sum: 130000 + 37500 = 167500 = total ✓
        # But let's make them way off
        p["models"] = [
            {"name": "claude-opus-4-6", "tokens": 50000, "cost": 130.0},
        ]
        # 50000 vs 167500 → > 20% off
        flags = check_token_consistency(p)
        soft = [f for f in flags if f.code == "MODEL_TOKEN_MISMATCH"]
        self.assertGreater(len(soft), 0)
        self.assertEqual(soft[0].severity, "soft")

    def test_non_dict_tokens_skipped(self):
        p = _make_clean_payload()
        p["tokens"] = 12345
        flags = check_token_consistency(p)
        self.assertEqual(flags, [])

    def test_zero_total_skipped(self):
        p = _make_clean_payload()
        p["tokens"]["total"] = 0
        flags = check_token_consistency(p)
        self.assertEqual(flags, [])


# ---------------------------------------------------------------------------
# check_active_days
# ---------------------------------------------------------------------------
class TestCheckActiveDays(unittest.TestCase):
    def test_matching_passes(self):
        p = _make_clean_payload()
        # activeDays=3, len(days)=3, streak=3 — all consistent
        flags = check_active_days(p)
        self.assertEqual(flags, [])

    def test_days_mismatch_soft(self):
        p = _make_clean_payload()
        p["activeDays"] = 5  # but only 3 days in days[]
        flags = check_active_days(p)
        soft = [f for f in flags if f.code == "ACTIVE_DAYS_MISMATCH"]
        self.assertGreater(len(soft), 0)
        self.assertEqual(soft[0].severity, "soft")

    def test_streak_exceeds_active_hard(self):
        p = _make_clean_payload()
        p["activeDays"] = 3
        p["streak"] = 10  # impossible
        flags = check_active_days(p)
        hard = [f for f in flags if f.code == "STREAK_EXCEEDS_ACTIVE"]
        self.assertEqual(len(hard), 1)
        self.assertEqual(hard[0].severity, "hard")

    def test_streak_equals_active_ok(self):
        p = _make_clean_payload()
        p["activeDays"] = 5
        p["streak"] = 5
        p["days"] = [{"date": f"2026-02-{i:02d}", "cost": 27.0} for i in range(1, 6)]
        flags = check_active_days(p)
        self.assertEqual(flags, [])

    def test_zero_active_days_no_streak_check(self):
        p = _make_clean_payload()
        p["activeDays"] = 0
        p["streak"] = 0
        p["days"] = []
        flags = check_active_days(p)
        self.assertEqual(flags, [])


# ---------------------------------------------------------------------------
# check_benford
# ---------------------------------------------------------------------------
class TestCheckBenford(unittest.TestCase):
    def test_fewer_than_10_skips(self):
        p = _make_clean_payload()
        p["days"] = [{"date": f"2026-02-{i:02d}", "cost": float(i * 10)} for i in range(1, 8)]
        flags = check_benford(p)
        self.assertEqual(flags, [])

    def test_uniform_distribution_flags(self):
        """Uniform leading digits (all same) should fail Benford."""
        p = _make_clean_payload()
        # All costs start with 5 → uniform, violates Benford
        p["days"] = [{"date": f"2026-02-{i:02d}", "cost": 50.0 + i * 0.01} for i in range(1, 26)]
        flags = check_benford(p)
        benford = [f for f in flags if f.code == "BENFORD_ANOMALY"]
        self.assertGreater(len(benford), 0)

    def test_natural_distribution_passes(self):
        """Benford-like distribution should pass."""
        import random
        random.seed(42)
        p = _make_clean_payload()
        # Generate Benford-distributed values
        costs = []
        for _ in range(30):
            # Exponential distribution tends to follow Benford's law
            val = 10 ** (random.random() * 3)  # 1 to 1000
            costs.append(round(val, 2))
        p["days"] = [{"date": f"2026-02-{(i % 28) + 1:02d}", "cost": c} for i, c in enumerate(costs)]
        p["activeDays"] = len(costs)
        p["cost"] = sum(costs)
        flags = check_benford(p)
        benford = [f for f in flags if f.code == "BENFORD_ANOMALY"]
        self.assertEqual(benford, [])

    def test_zero_costs_excluded(self):
        p = _make_clean_payload()
        p["days"] = [{"date": f"2026-02-{i:02d}", "cost": 0.0} for i in range(1, 20)]
        flags = check_benford(p)
        self.assertEqual(flags, [])  # all zeros → < 10 leading digits → skip


# ---------------------------------------------------------------------------
# verify_hash_chain
# ---------------------------------------------------------------------------
class TestVerifyHashChain(unittest.TestCase):
    def test_genesis_ok(self):
        flags = verify_hash_chain("genesis", None)
        self.assertEqual(flags, [])

    def test_none_prev_ok(self):
        flags = verify_hash_chain(None, None)
        self.assertEqual(flags, [])

    def test_matching_chain_ok(self):
        flags = verify_hash_chain("sha256:prev", "sha256:prev")
        self.assertEqual(flags, [])

    def test_mismatch_soft_flags(self):
        flags = verify_hash_chain("sha256:wrong", "sha256:actual")
        self.assertEqual(len(flags), 1)
        self.assertEqual(flags[0].code, "CHAIN_BREAK")
        self.assertEqual(flags[0].severity, "soft")

    def test_first_server_push_any_prev_ok(self):
        # Server has no previous hash, client declares something — OK
        flags = verify_hash_chain("sha256:some_local", None)
        self.assertEqual(flags, [])


# ---------------------------------------------------------------------------
# compute_trust_score
# ---------------------------------------------------------------------------
class TestComputeTrustScore(unittest.TestCase):
    def test_no_flags_perfect(self):
        self.assertEqual(compute_trust_score([]), 1.0)

    def test_one_hard_flag(self):
        flags = [TrustFlag("TEST", "hard", "test")]
        self.assertEqual(compute_trust_score(flags), 0.5)

    def test_one_soft_flag(self):
        flags = [TrustFlag("TEST", "soft", "test")]
        self.assertEqual(compute_trust_score(flags), 0.9)

    def test_multiple_soft_flags(self):
        flags = [TrustFlag("A", "soft", "a"), TrustFlag("B", "soft", "b")]
        self.assertEqual(compute_trust_score(flags), 0.8)

    def test_floor_at_zero(self):
        flags = [TrustFlag("A", "hard", "a"), TrustFlag("B", "hard", "b"), TrustFlag("C", "hard", "c")]
        self.assertEqual(compute_trust_score(flags), 0.0)

    def test_mixed_hard_and_soft(self):
        flags = [TrustFlag("A", "hard", "a"), TrustFlag("B", "soft", "b")]
        self.assertEqual(compute_trust_score(flags), 0.4)


# ---------------------------------------------------------------------------
# validate_trust (integration)
# ---------------------------------------------------------------------------
class TestValidateTrust(unittest.TestCase):
    def test_clean_payload_score_1(self):
        p = _make_clean_payload()
        h, flag_dicts, score, raw = validate_trust(p)
        self.assertTrue(h.startswith("sha256:"))
        self.assertEqual(score, 1.0)
        self.assertEqual(flag_dicts, [])

    def test_bad_model_blocked(self):
        p = _make_clean_payload()
        p["models"] = [{"name": "gpt-4", "tokens": 1000, "cost": 10.0}]
        h, flag_dicts, score, raw = validate_trust(p)
        hard_flags = [f for f in raw if f.severity == "hard"]
        self.assertGreater(len(hard_flags), 0)
        self.assertLess(score, 1.0)

    def test_returns_hash(self):
        p = _make_clean_payload()
        h, _, _, _ = validate_trust(p)
        self.assertTrue(h.startswith("sha256:"))
        self.assertEqual(len(h), 7 + 64)  # "sha256:" + 64 hex chars

    def test_flag_dicts_serializable(self):
        """flag_dicts should be plain dicts, JSON-serializable."""
        import json
        p = _make_clean_payload()
        p["models"].append({"name": "gpt-4", "tokens": 100, "cost": 1.0})
        _, flag_dicts, _, _ = validate_trust(p)
        serialized = json.dumps(flag_dicts)
        self.assertIsInstance(serialized, str)

    def test_prev_hash_chain_check(self):
        p = _make_clean_payload()
        p["_prevHash"] = "sha256:wrong"
        _, flag_dicts, score, _ = validate_trust(p, prev_hash="sha256:actual")
        chain_flags = [f for f in flag_dicts if f["code"] == "CHAIN_BREAK"]
        self.assertEqual(len(chain_flags), 1)

    def test_multiple_issues_compound(self):
        p = _make_clean_payload()
        p["models"] = [{"name": "gpt-4", "tokens": 1000, "cost": 10.0}]
        p["tokens"]["total"] = 999999
        p["activeDays"] = 3
        p["streak"] = 10
        _, _, score, raw = validate_trust(p)
        hard_count = sum(1 for f in raw if f.severity == "hard")
        self.assertGreaterEqual(hard_count, 3)
        self.assertEqual(score, 0.0)  # 3 hard flags = -1.5 → floor 0.0


if __name__ == "__main__":
    unittest.main()
