"""Tests for myburn.cloud — auth, push, profile endpoints."""
from __future__ import annotations

import json
import os
import secrets
import tempfile
import unittest
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch

# Set up temp cloud DB before importing cloud
_tmpdir = tempfile.mkdtemp()
os.environ["MYBURN_CLOUD_DB"] = os.path.join(_tmpdir, "cloud.db")
# Also set MYBURN_DATA_DIR for server imports
os.environ.setdefault("MYBURN_DATA_DIR", _tmpdir)

from myburn.cloud import (
    CLOUD_DB_PATH,
    _aggregate_user_stats,
    _apply_privacy_to_push,
    _compare_verdict,
    _get_cloud_db,
    _init_cloud_db,
    _parse_privacy,
    _render_challenge_html,
    _render_compare_html,
    _validate_payload,
    _render_profile_html,
    _render_leaderboard_html,
    _fmt_tokens_html,
    _fmt_leaderboard_value,
    register_cloud_routes,
)


def _make_valid_payload(**overrides) -> dict:
    """Create a minimal valid push payload with realistic cost/token ratios.

    Ratios: opus $13/MTok (between cache_read=$0.50 and output=$25),
    haiku $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, "thinkingPct": 30.0, "errorPct": 10.0, "avgDurationMs": 118889},
        "tools": {"Read": 4, "Bash": 3, "Edit": 3},
    }
    payload.update(overrides)
    return payload


def _create_test_user(conn, username="testdev", github_id=12345) -> dict:
    """Insert a user and return dict with id + plaintext api_token for test headers."""
    from myburn.cloud import _hash_token
    token = secrets.token_hex(16)
    token_hash = _hash_token(token)
    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    expires = "2099-12-31T23:59:59Z"  # far future for tests
    conn.execute(
        "INSERT INTO users (github_id, username, display_name, avatar_url, api_token, created_at, token_expires_at) "
        "VALUES (?, ?, ?, ?, ?, ?, ?)",
        (github_id, username, f"Test {username}", f"https://github.com/{username}.png", token_hash, now, expires),
    )
    conn.commit()
    row = conn.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()
    d = dict(row)
    d["api_token"] = token  # return plaintext for test Authorization headers
    return d


class TestValidatePayload(unittest.TestCase):
    """Test payload validation rules."""

    def test_valid_payload(self):
        self.assertIsNone(_validate_payload(_make_valid_payload()))

    def test_missing_version(self):
        p = _make_valid_payload()
        del p["version"]
        err = _validate_payload(p)
        self.assertIn("version", err)

    def test_missing_month(self):
        p = _make_valid_payload()
        del p["month"]
        err = _validate_payload(p)
        self.assertIn("month", err)

    def test_missing_cost(self):
        p = _make_valid_payload()
        del p["cost"]
        err = _validate_payload(p)
        self.assertIn("cost", err)

    def test_negative_cost(self):
        err = _validate_payload(_make_valid_payload(cost=-1))
        self.assertIn("cost", err)

    def test_insane_monthly_cost(self):
        err = _validate_payload(_make_valid_payload(cost=20000))
        self.assertIn("$15,000", err)

    def test_insane_daily_cost(self):
        payload = _make_valid_payload()
        payload["days"] = [{"date": "2026-02-01", "cost": 600}]
        err = _validate_payload(payload)
        self.assertIn("$500", err)

    def test_bad_month_format(self):
        err = _validate_payload(_make_valid_payload(month="February"))
        self.assertIn("month", err.lower())

    def test_bad_month_value(self):
        err = _validate_payload(_make_valid_payload(month="2026-13"))
        self.assertIn("month", err.lower())

    def test_zero_cost_ok(self):
        self.assertIsNone(_validate_payload(_make_valid_payload(cost=0)))

    def test_tokens_negative(self):
        payload = _make_valid_payload()
        payload["tokens"] = {"total": -1}
        err = _validate_payload(payload)
        self.assertIn("tokens", err)


class TestFmtTokensHtml(unittest.TestCase):
    def test_billions(self):
        self.assertEqual(_fmt_tokens_html(2_500_000_000), "2.50B")

    def test_millions(self):
        self.assertEqual(_fmt_tokens_html(42_000_000), "42.00M")

    def test_thousands(self):
        self.assertEqual(_fmt_tokens_html(167_500), "167.50K")

    def test_small(self):
        self.assertEqual(_fmt_tokens_html(999), "999")


class TestCloudDB(unittest.TestCase):
    """Test cloud DB init and operations."""

    @classmethod
    def setUpClass(cls):
        _init_cloud_db()

    def test_users_table_exists(self):
        conn = _get_cloud_db()
        tables = conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='users'"
        ).fetchone()
        conn.close()
        self.assertIsNotNone(tables)

    def test_pushes_table_exists(self):
        conn = _get_cloud_db()
        tables = conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='pushes'"
        ).fetchone()
        conn.close()
        self.assertIsNotNone(tables)

    def test_create_user(self):
        conn = _get_cloud_db()
        try:
            user = _create_test_user(conn, "dbtest1", 99991)
            self.assertEqual(user["username"], "dbtest1")
            self.assertIsNotNone(user["api_token"])
        finally:
            conn.execute("DELETE FROM users WHERE username = 'dbtest1'")
            conn.commit()
            conn.close()

    def test_unique_github_id(self):
        conn = _get_cloud_db()
        try:
            _create_test_user(conn, "uniq1", 99992)
            with self.assertRaises(Exception):
                _create_test_user(conn, "uniq2", 99992)
        finally:
            conn.execute("DELETE FROM users WHERE github_id = 99992")
            conn.commit()
            conn.close()

    def test_push_upsert(self):
        conn = _get_cloud_db()
        try:
            user = _create_test_user(conn, "pushtest", 99993)
            now = "2026-02-25T12:00:00Z"
            payload = json.dumps(_make_valid_payload())

            # First insert
            conn.execute(
                "INSERT INTO pushes (user_id, month, payload, cost, total_tokens, sessions, "
                "active_days, streak, cli_version, pushed_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
                (user["id"], "2026-02", payload, 135.0, 167500, 3, 3, 3, "0.1.0", now),
            )
            conn.commit()

            # Upsert (same user + month)
            conn.execute(
                "INSERT INTO pushes (user_id, month, payload, cost, total_tokens, sessions, "
                "active_days, streak, cli_version, pushed_at) VALUES (?,?,?,?,?,?,?,?,?,?) "
                "ON CONFLICT(user_id, month) DO UPDATE SET cost=excluded.cost, pushed_at=excluded.pushed_at",
                (user["id"], "2026-02", payload, 200.0, 200000, 5, 5, 5, "0.1.0", now),
            )
            conn.commit()

            row = conn.execute(
                "SELECT cost FROM pushes WHERE user_id = ? AND month = '2026-02'",
                (user["id"],),
            ).fetchone()
            self.assertAlmostEqual(row["cost"], 200.0)
        finally:
            conn.execute("DELETE FROM pushes WHERE user_id IN (SELECT id FROM users WHERE username='pushtest')")
            conn.execute("DELETE FROM users WHERE username = 'pushtest'")
            conn.commit()
            conn.close()


class TestRenderProfile(unittest.TestCase):
    """Test profile HTML rendering."""

    def test_basic_render(self):
        user = {
            "username": "gekko",
            "display_name": "Gekko Dev",
            "avatar_url": "https://github.com/gekko.png",
        }
        pushes = [{
            "month": "2026-02",
            "cost": 135.0,
            "total_tokens": 167500,
            "sessions": 3,
            "active_days": 3,
            "streak": 3,
            "payload": json.dumps(_make_valid_payload()),
        }]
        html = _render_profile_html(user, pushes)

        self.assertIn("@gekko", html)
        self.assertIn("Gekko Dev", html)
        self.assertIn("$135", html)
        self.assertIn("167.50K", html)
        self.assertIn("myburn.cc", html)
        self.assertIn("og:title", html)

    def test_empty_pushes(self):
        user = {"username": "newbie", "display_name": "New User", "avatar_url": ""}
        html = _render_profile_html(user, [])
        self.assertIn("@newbie", html)
        self.assertIn("No stats published yet", html)

    def test_no_avatar(self):
        user = {"username": "noavatar", "display_name": "No Avatar", "avatar_url": ""}
        html = _render_profile_html(user, [])
        self.assertNotIn('<img class="avatar"', html)

    def test_model_tags(self):
        user = {"username": "modeltag", "display_name": "Model", "avatar_url": ""}
        pushes = [{
            "month": "2026-02",
            "cost": 100.0,
            "total_tokens": 100000,
            "sessions": 1,
            "active_days": 1,
            "streak": 1,
            "payload": json.dumps(_make_valid_payload()),
        }]
        html = _render_profile_html(user, pushes)
        self.assertIn("opus-4-6", html)
        self.assertIn("model-tag", html)


class TestCloudRoutes(unittest.TestCase):
    """Integration tests for cloud API routes using FastAPI TestClient."""

    @classmethod
    def setUpClass(cls):
        _init_cloud_db()

        from fastapi import FastAPI
        from fastapi.testclient import TestClient

        cls.test_app = FastAPI()
        register_cloud_routes(cls.test_app)
        cls.client = TestClient(cls.test_app)

        # Create a test user
        conn = _get_cloud_db()
        cls.user = _create_test_user(conn, "routetest", 88888)
        conn.close()

    @classmethod
    def tearDownClass(cls):
        conn = _get_cloud_db()
        conn.execute("DELETE FROM pushes WHERE user_id IN (SELECT id FROM users WHERE username='routetest')")
        conn.execute("DELETE FROM users WHERE username = 'routetest'")
        conn.commit()
        conn.close()

    # --- POST /api/auth/github ---
    @patch("myburn.cloud._fetch_github_user")
    def test_auth_github_new_user(self, mock_gh):
        mock_gh.return_value = {
            "id": 77777,
            "login": "newghuser",
            "name": "New GH User",
            "avatar_url": "https://github.com/new.png",
        }
        resp = self.client.post("/api/auth/github", json={"github_token": "fake_token"})
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["username"], "newghuser")
        self.assertIn("token", data)
        self.assertTrue(len(data["token"]) > 10)

        # Cleanup
        conn = _get_cloud_db()
        conn.execute("DELETE FROM users WHERE username = 'newghuser'")
        conn.commit()
        conn.close()

    @patch("myburn.cloud._fetch_github_user")
    def test_auth_github_existing_user(self, mock_gh):
        mock_gh.return_value = {
            "id": 88888,  # same github_id as routetest
            "login": "routetest",
            "name": "Route Test Updated",
            "avatar_url": "https://github.com/updated.png",
        }
        resp = self.client.post("/api/auth/github", json={"github_token": "fake"})
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["username"], "routetest")
        # Token rotated
        self.assertNotEqual(data["token"], self.user["api_token"])
        # Update class reference for subsequent tests
        self.__class__.user["api_token"] = data["token"]

    def test_auth_github_missing_token(self):
        resp = self.client.post("/api/auth/github", json={})
        self.assertEqual(resp.status_code, 400)

    @patch("myburn.cloud._fetch_github_user")
    def test_auth_github_returns_expires_at(self, mock_gh):
        mock_gh.return_value = {
            "id": 77776, "login": "expirytest",
            "name": "Expiry Test", "avatar_url": "",
        }
        resp = self.client.post("/api/auth/github", json={"github_token": "fake"})
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertIn("expires_at", data)
        self.assertTrue(data["expires_at"].endswith("Z"))
        # Cleanup
        conn = _get_cloud_db()
        conn.execute("DELETE FROM users WHERE username = 'expirytest'")
        conn.commit()
        conn.close()

    # --- POST /api/auth/revoke ---
    def test_revoke_no_auth(self):
        resp = self.client.post("/api/auth/revoke")
        self.assertEqual(resp.status_code, 401)

    def test_revoke_invalidates_token(self):
        """Revoking a token makes it immediately invalid."""
        conn = _get_cloud_db()
        user = _create_test_user(conn, "revoketest", 99999)
        conn.close()

        # Token works before revoke
        resp = self.client.post(
            "/api/push", json=_make_valid_payload(),
            headers={"Authorization": f"Bearer {user['api_token']}"},
        )
        self.assertEqual(resp.status_code, 200)

        # Revoke
        resp = self.client.post(
            "/api/auth/revoke",
            headers={"Authorization": f"Bearer {user['api_token']}"},
        )
        self.assertEqual(resp.status_code, 200)
        self.assertTrue(resp.json()["ok"])

        # Token no longer works
        resp = self.client.post(
            "/api/push", json=_make_valid_payload(),
            headers={"Authorization": f"Bearer {user['api_token']}"},
        )
        self.assertEqual(resp.status_code, 401)

        # Cleanup
        conn = _get_cloud_db()
        conn.execute("DELETE FROM pushes WHERE user_id = ?", (user["id"],))
        conn.execute("DELETE FROM users WHERE username = 'revoketest'")
        conn.commit()
        conn.close()

    def test_expired_token_rejected(self):
        """Expired token should return 401 with expiry message."""
        from myburn.cloud import _hash_token
        conn = _get_cloud_db()
        token = secrets.token_hex(16)
        token_hash = _hash_token(token)
        now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        conn.execute(
            "INSERT INTO users (github_id, username, display_name, avatar_url, api_token, created_at, token_expires_at) "
            "VALUES (?, ?, ?, ?, ?, ?, ?)",
            (99998, "expireduser", "Expired", "", token_hash, now, "2020-01-01T00:00:00Z"),
        )
        conn.commit()
        conn.close()

        resp = self.client.post(
            "/api/push", json=_make_valid_payload(),
            headers={"Authorization": f"Bearer {token}"},
        )
        self.assertEqual(resp.status_code, 401)
        self.assertIn("expired", resp.json()["detail"].lower())

        # Cleanup
        conn = _get_cloud_db()
        conn.execute("DELETE FROM users WHERE username = 'expireduser'")
        conn.commit()
        conn.close()

    # --- POST /api/push ---
    def test_push_success(self):
        payload = _make_valid_payload()
        resp = self.client.post(
            "/api/push",
            json=payload,
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertTrue(data["ok"])
        self.assertIn("profile_url", data)
        self.assertIn("routetest", data["profile_url"])

    def test_push_no_auth(self):
        resp = self.client.post("/api/push", json=_make_valid_payload())
        self.assertEqual(resp.status_code, 401)

    def test_push_bad_token(self):
        resp = self.client.post(
            "/api/push",
            json=_make_valid_payload(),
            headers={"Authorization": "Bearer invalid_token"},
        )
        self.assertEqual(resp.status_code, 401)

    def test_push_invalid_payload(self):
        bad = {"version": "1.0"}  # missing required fields
        resp = self.client.post(
            "/api/push",
            json=bad,
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        self.assertEqual(resp.status_code, 422)

    def test_push_upsert(self):
        """Re-pushing same month updates data."""
        payload1 = _make_valid_payload(cost=100.0)
        payload1["days"] = [
            {"date": "2026-02-01", "cost": 40.0},
            {"date": "2026-02-02", "cost": 35.0},
            {"date": "2026-02-03", "cost": 25.0},
        ]
        self.client.post(
            "/api/push", json=payload1,
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )

        payload2 = _make_valid_payload(cost=200.0)
        payload2["days"] = [
            {"date": "2026-02-01", "cost": 80.0},
            {"date": "2026-02-02", "cost": 70.0},
            {"date": "2026-02-03", "cost": 50.0},
        ]
        resp = self.client.post(
            "/api/push", json=payload2,
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        self.assertEqual(resp.status_code, 200)

        # Verify latest cost
        conn = _get_cloud_db()
        row = conn.execute(
            "SELECT cost FROM pushes WHERE user_id = ? AND month = '2026-02'",
            (self.user["id"],),
        ).fetchone()
        conn.close()
        self.assertAlmostEqual(row["cost"], 200.0)

    # --- GET /api/u/{username} ---
    def test_api_profile(self):
        resp = self.client.get("/api/u/routetest")
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["username"], "routetest")
        self.assertIn("months", data)

    def test_api_profile_includes_payload_hash(self):
        """Profile JSON months should include payload_hash field."""
        resp = self.client.get("/api/u/routetest")
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        for m in data["months"]:
            self.assertIn("payload_hash", m)

    def test_api_profile_not_found(self):
        resp = self.client.get("/api/u/nonexistent_user_12345")
        self.assertEqual(resp.status_code, 404)

    # --- GET /u/{username} ---
    def test_html_profile(self):
        resp = self.client.get("/u/routetest")
        self.assertEqual(resp.status_code, 200)
        self.assertIn("text/html", resp.headers["content-type"])
        self.assertIn("@routetest", resp.text)

    def test_html_profile_not_found(self):
        resp = self.client.get("/u/nonexistent_user_12345")
        self.assertEqual(resp.status_code, 404)

    # --- E2E: auth → push → profile ---
    @patch("myburn.cloud._fetch_github_user")
    def test_e2e_flow(self, mock_gh):
        """Full flow: auth → push → view profile (JSON + HTML)."""
        mock_gh.return_value = {
            "id": 66666,
            "login": "e2euser",
            "name": "E2E Dev",
            "avatar_url": "https://github.com/e2e.png",
        }

        # 1. Auth
        auth_resp = self.client.post("/api/auth/github", json={"github_token": "fake"})
        self.assertEqual(auth_resp.status_code, 200)
        token = auth_resp.json()["token"]

        # 2. Push
        payload = _make_valid_payload(cost=42.0)
        payload["days"] = [
            {"date": "2026-02-01", "cost": 15.0},
            {"date": "2026-02-02", "cost": 15.0},
            {"date": "2026-02-03", "cost": 12.0},
        ]
        push_resp = self.client.post(
            "/api/push", json=payload,
            headers={"Authorization": f"Bearer {token}"},
        )
        self.assertEqual(push_resp.status_code, 200)
        self.assertTrue(push_resp.json()["ok"])

        # 3. JSON profile
        json_resp = self.client.get("/api/u/e2euser")
        self.assertEqual(json_resp.status_code, 200)
        profile = json_resp.json()
        self.assertEqual(profile["username"], "e2euser")
        self.assertEqual(len(profile["months"]), 1)
        self.assertAlmostEqual(profile["months"][0]["cost"], 42.0)

        # 4. HTML profile
        html_resp = self.client.get("/u/e2euser")
        self.assertEqual(html_resp.status_code, 200)
        self.assertIn("@e2euser", html_resp.text)
        self.assertIn("$42", html_resp.text)

        # Cleanup
        conn = _get_cloud_db()
        conn.execute("DELETE FROM pushes WHERE user_id IN (SELECT id FROM users WHERE username='e2euser')")
        conn.execute("DELETE FROM users WHERE username = 'e2euser'")
        conn.commit()
        conn.close()


class TestTrustIntegration(unittest.TestCase):
    """Trust infrastructure integration tests via push endpoint."""

    @classmethod
    def setUpClass(cls):
        _init_cloud_db()

        from fastapi import FastAPI
        from fastapi.testclient import TestClient

        cls.test_app = FastAPI()
        register_cloud_routes(cls.test_app)
        cls.client = TestClient(cls.test_app)

        conn = _get_cloud_db()
        cls.user = _create_test_user(conn, "trusttest", 77770)
        conn.close()

    @classmethod
    def tearDownClass(cls):
        conn = _get_cloud_db()
        conn.execute("DELETE FROM pushes WHERE user_id IN (SELECT id FROM users WHERE username='trusttest')")
        conn.execute("DELETE FROM users WHERE username = 'trusttest'")
        conn.commit()
        conn.close()

    def _push(self, payload):
        return self.client.post(
            "/api/push",
            json=payload,
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )

    def test_push_unknown_model_422(self):
        """Push with non-Claude model should be rejected (HARD flag)."""
        payload = _make_valid_payload()
        payload["models"] = [{"name": "gpt-4-turbo", "tokens": 1000, "cost": 10.0}]
        resp = self._push(payload)
        self.assertEqual(resp.status_code, 422)
        self.assertIn("Unknown model", resp.json()["detail"])

    def test_push_token_mismatch_422(self):
        """Push with token sub-fields not summing to total should be rejected."""
        payload = _make_valid_payload()
        payload["tokens"]["total"] = 999999  # doesn't match sub-fields
        resp = self._push(payload)
        self.assertEqual(resp.status_code, 422)
        self.assertIn("Trust check failed", resp.json()["detail"])

    def test_push_streak_exceeds_active_422(self):
        """Push with streak > activeDays should be rejected."""
        payload = _make_valid_payload()
        payload["streak"] = 100
        payload["activeDays"] = 3
        resp = self._push(payload)
        self.assertEqual(resp.status_code, 422)

    def test_push_daily_sum_mismatch_422(self):
        """Push with daily costs way off from total should be rejected."""
        payload = _make_valid_payload()
        payload["cost"] = 135.0
        payload["days"] = [{"date": "2026-02-01", "cost": 1.0}]
        resp = self._push(payload)
        self.assertEqual(resp.status_code, 422)

    def test_push_valid_includes_trust_score(self):
        """Valid push should return trust_score in response."""
        payload = _make_valid_payload(month="2026-01")
        resp = self._push(payload)
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertTrue(data["ok"])
        self.assertIn("trust_score", data)
        self.assertEqual(data["trust_score"], 1.0)

    def test_push_suspicious_pricing_succeeds(self):
        """Push with slightly off pricing should succeed (soft flag only)."""
        payload = _make_valid_payload(month="2025-12")
        # Model cost sum doesn't match total by > 20%
        payload["cost"] = 500.0
        payload["models"] = [
            {"name": "claude-opus-4-6", "tokens": 130000, "cost": 130.0},
        ]
        # Adjust days to match new total
        payload["days"] = [
            {"date": "2025-12-01", "cost": 200.0},
            {"date": "2025-12-02", "cost": 200.0},
            {"date": "2025-12-03", "cost": 100.0},
        ]
        resp = self._push(payload)
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertTrue(data["ok"])
        # Score < 1.0 due to soft flag
        self.assertLess(data["trust_score"], 1.0)

    def test_sequential_pushes_build_hash_chain(self):
        """Sequential pushes should store payload_hash that can be used for chain verification."""
        # First push
        p1 = _make_valid_payload(month="2025-10")
        resp1 = self._push(p1)
        self.assertEqual(resp1.status_code, 200)

        # Check hash stored in DB
        conn = _get_cloud_db()
        row1 = conn.execute(
            "SELECT payload_hash, trust_score FROM pushes WHERE user_id = ? AND month = '2025-10'",
            (self.user["id"],),
        ).fetchone()
        conn.close()
        self.assertIsNotNone(row1["payload_hash"])
        self.assertTrue(row1["payload_hash"].startswith("sha256:"))

        # Second push
        p2 = _make_valid_payload(month="2025-11")
        resp2 = self._push(p2)
        self.assertEqual(resp2.status_code, 200)

        conn = _get_cloud_db()
        row2 = conn.execute(
            "SELECT payload_hash, prev_hash FROM pushes WHERE user_id = ? AND month = '2025-11'",
            (self.user["id"],),
        ).fetchone()
        conn.close()
        self.assertIsNotNone(row2["payload_hash"])


class TestLeaderboard(unittest.TestCase):
    """Integration tests for leaderboard API + HTML routes."""

    @classmethod
    def setUpClass(cls):
        _init_cloud_db()

        from fastapi import FastAPI
        from fastapi.testclient import TestClient

        cls.test_app = FastAPI()
        register_cloud_routes(cls.test_app)
        cls.client = TestClient(cls.test_app)

        # Create 5 users with varied pushes
        conn = _get_cloud_db()

        # Current month + last month
        now = datetime.now(timezone.utc)
        cls.current_month = now.strftime("%Y-%m")
        if now.month == 1:
            cls.last_month = f"{now.year - 1}-12"
        else:
            cls.last_month = f"{now.year}-{now.month - 1:02d}"

        cls.users = []
        user_data = [
            # (username, github_id, current_cost, current_tokens, current_sessions, current_days, current_streak,
            #  last_cost, last_tokens, last_sessions, last_days, last_streak)
            ("lb_alpha", 50001, 1000.0, 5000000, 50, 20, 15, 500.0, 2000000, 30, 15, 10),
            ("lb_bravo", 50002, 800.0, 8000000, 40, 18, 20, 600.0, 3000000, 35, 16, 8),
            ("lb_charlie", 50003, 1200.0, 3000000, 60, 22, 10, 400.0, 1000000, 20, 10, 5),
            ("lb_delta", 50004, 500.0, 10000000, 30, 12, 25, 300.0, 500000, 15, 8, 3),
            ("lb_echo", 50005, 200.0, 1000000, 20, 8, 5, 100.0, 400000, 10, 5, 2),
        ]

        for uname, gid, cc, ct, cs, cd, cst, lc, lt, ls, ld, lst in user_data:
            user = _create_test_user(conn, uname, gid)
            cls.users.append(user)

            now_str = "2026-02-25T12:00:00Z"
            payload = json.dumps(_make_valid_payload(cost=cc))

            # Current month push
            conn.execute(
                "INSERT INTO pushes (user_id, month, payload, cost, total_tokens, sessions, "
                "active_days, streak, cli_version, pushed_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
                (user["id"], cls.current_month, payload, cc, ct, cs, cd, cst, "0.1.0", now_str),
            )

            # Last month push
            conn.execute(
                "INSERT INTO pushes (user_id, month, payload, cost, total_tokens, sessions, "
                "active_days, streak, cli_version, pushed_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
                (user["id"], cls.last_month, payload, lc, lt, ls, ld, lst, "0.1.0", now_str),
            )

        conn.commit()
        conn.close()

    @classmethod
    def tearDownClass(cls):
        conn = _get_cloud_db()
        for user in cls.users:
            conn.execute("DELETE FROM pushes WHERE user_id = ?", (user["id"],))
            conn.execute("DELETE FROM users WHERE id = ?", (user["id"],))
        conn.commit()
        conn.close()

    # --- 1. Default params ---
    def test_default(self):
        resp = self.client.get("/api/leaderboard")
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["metric"], "cost")
        self.assertEqual(data["period"], "current")
        self.assertIsInstance(data["entries"], list)

    # --- 2. Ordered by cost ---
    def test_by_cost(self):
        resp = self.client.get("/api/leaderboard?metric=cost&period=current")
        data = resp.json()
        values = [e["value"] for e in data["entries"]]
        # Filter to our test users
        our_entries = [e for e in data["entries"] if e["username"].startswith("lb_")]
        our_values = [e["value"] for e in our_entries]
        self.assertEqual(our_values, sorted(our_values, reverse=True))
        # Charlie (1200) > Alpha (1000) > Bravo (800)
        self.assertEqual(our_entries[0]["username"], "lb_charlie")

    # --- 3. Ordered by tokens ---
    def test_by_tokens(self):
        resp = self.client.get("/api/leaderboard?metric=tokens&period=current")
        data = resp.json()
        our_entries = [e for e in data["entries"] if e["username"].startswith("lb_")]
        # Delta (10M) > Bravo (8M) > Alpha (5M) > Charlie (3M) > Echo (1M)
        self.assertEqual(our_entries[0]["username"], "lb_delta")

    # --- 4. Ordered by sessions ---
    def test_by_sessions(self):
        resp = self.client.get("/api/leaderboard?metric=sessions&period=current")
        data = resp.json()
        our_entries = [e for e in data["entries"] if e["username"].startswith("lb_")]
        # Charlie (60) > Alpha (50) > Bravo (40) > Delta (30) > Echo (20)
        self.assertEqual(our_entries[0]["username"], "lb_charlie")

    # --- 5. Ordered by active_days ---
    def test_by_active_days(self):
        resp = self.client.get("/api/leaderboard?metric=active_days&period=current")
        data = resp.json()
        our_entries = [e for e in data["entries"] if e["username"].startswith("lb_")]
        # Charlie (22) > Alpha (20) > Bravo (18) > Delta (12) > Echo (8)
        self.assertEqual(our_entries[0]["username"], "lb_charlie")

    # --- 6. Ordered by streak ---
    def test_by_streak(self):
        resp = self.client.get("/api/leaderboard?metric=streak&period=current")
        data = resp.json()
        our_entries = [e for e in data["entries"] if e["username"].startswith("lb_")]
        # Delta (25) > Bravo (20) > Alpha (15) > Charlie (10) > Echo (5)
        self.assertEqual(our_entries[0]["username"], "lb_delta")

    # --- 7. Current period ---
    def test_period_current(self):
        resp = self.client.get("/api/leaderboard?period=current")
        data = resp.json()
        self.assertEqual(data["period"], "current")
        our = [e for e in data["entries"] if e["username"].startswith("lb_")]
        # Charlie has highest cost (1200) in current month
        self.assertEqual(our[0]["username"], "lb_charlie")
        self.assertAlmostEqual(our[0]["value"], 1200.0)

    # --- 8. Last period ---
    def test_period_last(self):
        resp = self.client.get("/api/leaderboard?period=last")
        data = resp.json()
        self.assertEqual(data["period"], "last")
        our = [e for e in data["entries"] if e["username"].startswith("lb_")]
        # Bravo has highest cost (600) in last month
        self.assertEqual(our[0]["username"], "lb_bravo")
        self.assertAlmostEqual(our[0]["value"], 600.0)

    # --- 9. All-time SUM ---
    def test_period_alltime(self):
        resp = self.client.get("/api/leaderboard?metric=cost&period=alltime")
        data = resp.json()
        self.assertEqual(data["period"], "alltime")
        self.assertEqual(data["period_label"], "All Time")
        our = [e for e in data["entries"] if e["username"].startswith("lb_")]
        # Charlie: 1200+400=1600, Alpha: 1000+500=1500, Bravo: 800+600=1400
        self.assertEqual(our[0]["username"], "lb_charlie")
        self.assertAlmostEqual(our[0]["value"], 1600.0)

    # --- 10. All-time streak uses MAX ---
    def test_alltime_streak_max(self):
        resp = self.client.get("/api/leaderboard?metric=streak&period=alltime")
        data = resp.json()
        our = [e for e in data["entries"] if e["username"].startswith("lb_")]
        # Delta: MAX(25, 3) = 25
        delta = [e for e in our if e["username"] == "lb_delta"][0]
        self.assertEqual(delta["value"], 25)
        # NOT SUM (25 + 3 = 28)
        self.assertNotEqual(delta["value"], 28)

    # --- 11. Limit ---
    def test_limit(self):
        resp = self.client.get("/api/leaderboard?limit=2")
        data = resp.json()
        self.assertLessEqual(len(data["entries"]), 2)

    # --- 12. Invalid metric ---
    def test_invalid_metric(self):
        resp = self.client.get("/api/leaderboard?metric=invalid")
        self.assertEqual(resp.status_code, 400)

    # --- 13. Invalid period ---
    def test_invalid_period(self):
        resp = self.client.get("/api/leaderboard?period=invalid")
        self.assertEqual(resp.status_code, 400)

    # --- 14. Percentile ---
    def test_percentile(self):
        resp = self.client.get("/api/leaderboard?metric=cost&period=current")
        data = resp.json()
        if data["entries"]:
            # Rank 1 should have highest percentile
            top = data["entries"][0]
            total = data["total_users"]
            expected_pct = round((1 - (1 - 1) / total) * 100, 1)
            self.assertEqual(top["percentile"], expected_pct)

    # --- 15. Total users ---
    def test_total_users(self):
        resp = self.client.get("/api/leaderboard?period=current")
        data = resp.json()
        # At least our 5 test users
        self.assertGreaterEqual(data["total_users"], 5)

    # --- 16. Empty ---
    def test_empty(self):
        # Use a period that likely has no data (far future month — we use a custom metric/month combo)
        # Since we can't control the period resolution, test with last month if no other users pushed
        # Best approach: just verify the structure even if entries exist
        resp = self.client.get("/api/leaderboard?period=current")
        data = resp.json()
        self.assertIn("entries", data)
        self.assertIn("total_users", data)
        self.assertIsInstance(data["entries"], list)

    # --- 17. With auth → you field present ---
    def test_with_auth(self):
        token = self.users[0]["api_token"]
        resp = self.client.get(
            "/api/leaderboard?period=current",
            headers={"Authorization": f"Bearer {token}"},
        )
        data = resp.json()
        self.assertIsNotNone(data["you"])
        self.assertIn("rank", data["you"])
        self.assertIn("value", data["you"])
        self.assertIn("percentile", data["you"])
        self.assertEqual(data["you"]["username"], "lb_alpha")

    # --- 18. Without auth → you null ---
    def test_without_auth(self):
        resp = self.client.get("/api/leaderboard?period=current")
        data = resp.json()
        self.assertIsNone(data["you"])

    # --- 19. You outside top N ---
    def test_you_outside_top(self):
        # Echo has lowest cost (200), use limit=2 so they're outside top
        token = self.users[4]["api_token"]  # lb_echo
        resp = self.client.get(
            "/api/leaderboard?metric=cost&period=current&limit=2",
            headers={"Authorization": f"Bearer {token}"},
        )
        data = resp.json()
        self.assertIsNotNone(data["you"])
        # Echo should not be in the top 2 entries
        entry_usernames = [e["username"] for e in data["entries"]]
        self.assertNotIn("lb_echo", entry_usernames)
        # But their rank should be > 2
        self.assertGreater(data["you"]["rank"], 2)

    # --- 20. HTML 200 ---
    def test_html_200(self):
        resp = self.client.get("/leaderboard")
        self.assertEqual(resp.status_code, 200)
        self.assertIn("text/html", resp.headers["content-type"])

    # --- 21. HTML contains usernames ---
    def test_html_usernames(self):
        resp = self.client.get("/leaderboard?metric=cost&period=current")
        html = resp.text
        # Charlie should be #1 for cost/current
        self.assertIn("lb_charlie", html)

    # --- 22. HTML OG tags ---
    def test_html_og_tags(self):
        resp = self.client.get("/leaderboard")
        html = resp.text
        self.assertIn("og:title", html)
        self.assertIn("MyBurn Leaderboard", html)


class TestPrivacy(unittest.TestCase):
    """Tests for privacy helpers and endpoints."""

    def test_parse_privacy_empty(self):
        self.assertEqual(_parse_privacy({"privacy": "{}"}), {})
        self.assertEqual(_parse_privacy({"privacy": None}), {})
        self.assertEqual(_parse_privacy({}), {})

    def test_parse_privacy_valid(self):
        priv = _parse_privacy({"privacy": '{"hideCost": true, "hideTokens": false}'})
        self.assertEqual(priv, {"hideCost": True, "hideTokens": False})

    def test_parse_privacy_ignores_unknown(self):
        priv = _parse_privacy({"privacy": '{"hideCost": true, "evil": true}'})
        self.assertNotIn("evil", priv)
        self.assertTrue(priv["hideCost"])

    def test_apply_privacy_hides_cost(self):
        push = {"cost": 100.0, "total_tokens": 5000, "sessions": 3, "active_days": 5, "streak": 2}
        result = _apply_privacy_to_push(push, {"hideCost": True})
        self.assertIsNone(result["cost"])
        self.assertEqual(result["total_tokens"], 5000)

    def test_apply_privacy_hides_tokens(self):
        push = {"cost": 100.0, "total_tokens": 5000, "sessions": 3, "active_days": 5, "streak": 2}
        result = _apply_privacy_to_push(push, {"hideTokens": True})
        self.assertIsNone(result["total_tokens"])
        self.assertEqual(result["cost"], 100.0)

    def test_apply_privacy_hides_days(self):
        push = {"cost": 100.0, "total_tokens": 5000, "sessions": 3, "active_days": 5, "streak": 2}
        result = _apply_privacy_to_push(push, {"hideDays": True})
        self.assertIsNone(result["active_days"])
        self.assertIsNone(result["streak"])


class TestPrivacyRoutes(unittest.TestCase):
    """Integration tests for privacy API endpoints."""

    @classmethod
    def setUpClass(cls):
        _init_cloud_db()

        from fastapi import FastAPI
        from fastapi.testclient import TestClient

        cls.test_app = FastAPI()
        register_cloud_routes(cls.test_app)
        cls.client = TestClient(cls.test_app)

        conn = _get_cloud_db()
        cls.user = _create_test_user(conn, "privtest", 55551)
        conn.close()

    @classmethod
    def tearDownClass(cls):
        conn = _get_cloud_db()
        conn.execute("DELETE FROM pushes WHERE user_id IN (SELECT id FROM users WHERE username='privtest')")
        conn.execute("DELETE FROM users WHERE username = 'privtest'")
        conn.commit()
        conn.close()

    def test_get_privacy_default(self):
        # Reset privacy to empty first (tests run alphabetically, shared user)
        self.client.post(
            "/api/privacy", json={},
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        resp = self.client.get(
            "/api/privacy",
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.json(), {})

    def test_set_privacy(self):
        resp = self.client.post(
            "/api/privacy",
            json={"hideCost": True, "hideTokens": True},
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        self.assertEqual(resp.status_code, 200)
        self.assertTrue(resp.json()["ok"])
        self.assertTrue(resp.json()["privacy"]["hideCost"])

    def test_get_privacy_after_set(self):
        # Set
        self.client.post(
            "/api/privacy",
            json={"hideCost": True},
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        # Get
        resp = self.client.get(
            "/api/privacy",
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        self.assertEqual(resp.status_code, 200)
        self.assertTrue(resp.json()["hideCost"])

    def test_privacy_no_auth(self):
        resp = self.client.get("/api/privacy")
        self.assertEqual(resp.status_code, 401)

    def test_profile_redacted_with_privacy(self):
        """Profile should redact fields when privacy is set."""
        # Set privacy
        self.client.post(
            "/api/privacy",
            json={"hideCost": True, "hideTokens": True},
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        # Push some data
        self.client.post(
            "/api/push",
            json=_make_valid_payload(),
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        # Check profile
        resp = self.client.get("/api/u/privtest")
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        for m in data["months"]:
            self.assertIsNone(m["cost"])
            self.assertIsNone(m["total_tokens"])

    def test_profile_html_shows_hidden(self):
        """HTML profile should show 'Hidden' for redacted fields."""
        # Ensure privacy is set and data exists
        self.client.post(
            "/api/privacy", json={"hideCost": True},
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        self.client.post(
            "/api/push", json=_make_valid_payload(),
            headers={"Authorization": f"Bearer {self.user['api_token']}"},
        )
        resp = self.client.get("/u/privtest")
        self.assertEqual(resp.status_code, 200)
        self.assertIn("Hidden", resp.text)


# ---------------------------------------------------------------------------
# Compare — unit tests
# ---------------------------------------------------------------------------
class TestCompareHelpers(unittest.TestCase):
    """Unit tests for compare helper functions."""

    def test_aggregate_user_stats_basic(self):
        pushes = [
            {"cost": 100.0, "total_tokens": 5000, "sessions": 3, "active_days": 5, "streak": 2},
            {"cost": 200.0, "total_tokens": 8000, "sessions": 7, "active_days": 10, "streak": 4},
        ]
        s = _aggregate_user_stats(pushes, {})
        self.assertEqual(s["cost"], 300.0)
        self.assertEqual(s["tokens"], 13000)
        self.assertEqual(s["sessions"], 10)
        self.assertEqual(s["active_days"], 15)
        self.assertEqual(s["streak"], 4)
        self.assertEqual(s["months"], 2)

    def test_aggregate_respects_privacy(self):
        pushes = [{"cost": 100.0, "total_tokens": 5000, "sessions": 3, "active_days": 5, "streak": 2}]
        s = _aggregate_user_stats(pushes, {"hideCost": True, "hideTokens": True})
        self.assertIsNone(s["cost"])
        self.assertIsNone(s["tokens"])
        self.assertEqual(s["sessions"], 3)

    def test_aggregate_hide_days(self):
        pushes = [{"cost": 50.0, "total_tokens": 1000, "sessions": 1, "active_days": 3, "streak": 3}]
        s = _aggregate_user_stats(pushes, {"hideDays": True})
        self.assertIsNone(s["active_days"])
        self.assertIsNone(s["streak"])

    def test_verdict_higher_burns_harder(self):
        v = _compare_verdict("alice", "bob", {"cost": 4200}, {"cost": 2100})
        self.assertIn("alice", v)
        self.assertIn("2.0x", v)

    def test_verdict_dead_heat(self):
        v = _compare_verdict("a", "b", {"cost": 100}, {"cost": 100})
        self.assertIn("Dead heat", v)

    def test_verdict_hidden_cost(self):
        v = _compare_verdict("a", "b", {"cost": None}, {"cost": 100})
        self.assertEqual(v, "")

    def test_verdict_zero_costs(self):
        v = _compare_verdict("a", "b", {"cost": 0}, {"cost": 0})
        self.assertIn("Rookies", v)

    def test_verdict_one_zero(self):
        v = _compare_verdict("alice", "bob", {"cost": 500}, {"cost": 0})
        self.assertIn("alice", v)
        self.assertIn("watches", v)


class TestRenderCompare(unittest.TestCase):
    """Unit tests for compare HTML rendering."""

    def _make_user(self, username, display_name=None):
        return {
            "username": username,
            "display_name": display_name or username.title(),
            "avatar_url": f"https://github.com/{username}.png",
        }

    def _make_pushes(self, cost=100.0, tokens=5000, sessions=3, days=5, streak=2):
        return [{"cost": cost, "total_tokens": tokens, "sessions": sessions,
                 "active_days": days, "streak": streak, "payload": "{}"}]

    def test_basic_render(self):
        html = _render_compare_html(
            self._make_user("alice"), self._make_pushes(cost=4200), {},
            self._make_user("bob"), self._make_pushes(cost=2100), {},
        )
        self.assertIn("@alice", html)
        self.assertIn("@bob", html)
        self.assertIn("VS", html)
        self.assertIn("og:title", html)
        self.assertIn("Who Burned More", html)

    def test_verdict_in_html(self):
        html = _render_compare_html(
            self._make_user("alice"), self._make_pushes(cost=4200), {},
            self._make_user("bob"), self._make_pushes(cost=2100), {},
        )
        self.assertIn("2.0x", html)

    def test_privacy_hides_delta(self):
        html = _render_compare_html(
            self._make_user("alice"), self._make_pushes(cost=4200), {"hideCost": True},
            self._make_user("bob"), self._make_pushes(cost=2100), {},
        )
        self.assertIn("Hidden", html)
        # Should NOT contain verdict (cost hidden = no leak)
        self.assertNotIn("2.0x", html)

    def test_share_button_present(self):
        html = _render_compare_html(
            self._make_user("a"), self._make_pushes(), {},
            self._make_user("b"), self._make_pushes(), {},
        )
        self.assertIn("Share on X", html)
        self.assertIn("intent/tweet", html)

    def test_profile_links(self):
        html = _render_compare_html(
            self._make_user("alice"), self._make_pushes(), {},
            self._make_user("bob"), self._make_pushes(), {},
        )
        self.assertIn("/u/alice", html)
        self.assertIn("/u/bob", html)


class TestRenderChallenge(unittest.TestCase):
    """Unit tests for challenge HTML rendering."""

    def test_challenge_render(self):
        user = {"username": "alice", "display_name": "Alice", "avatar_url": "https://github.com/alice.png"}
        pushes = [{"cost": 4200.0, "total_tokens": 1000000, "sessions": 50,
                    "active_days": 20, "streak": 10, "payload": "{}"}]
        html = _render_challenge_html(user, pushes, {})
        self.assertIn("challenged you", html)
        self.assertIn("$4,200", html)
        self.assertIn("pip install myburn", html)
        self.assertIn("myburn push", html)
        self.assertIn("og:title", html)

    def test_challenge_privacy(self):
        user = {"username": "alice", "display_name": "Alice", "avatar_url": ""}
        pushes = [{"cost": None, "total_tokens": 5000, "sessions": 3,
                    "active_days": 5, "streak": 2, "payload": "{}"}]
        html = _render_challenge_html(user, pushes, {"hideCost": True})
        self.assertIn("a mystery amount", html)


# ---------------------------------------------------------------------------
# Compare — integration tests
# ---------------------------------------------------------------------------
class TestShareButtons(unittest.TestCase):
    """Unit tests for share buttons in rendered HTML."""

    def test_profile_share_button(self):
        user = {"username": "testshare", "display_name": "Test", "avatar_url": ""}
        pushes = [{"month": "2026-02", "cost": 100.0, "total_tokens": 5000, "sessions": 3,
                    "active_days": 5, "streak": 2, "payload": "{}"}]
        html = _render_profile_html(user, pushes, {})
        self.assertIn("Share Profile", html)
        self.assertIn("intent/tweet", html)
        self.assertIn("myburn.cc/u/testshare", html)

    def test_profile_share_respects_privacy(self):
        user = {"username": "sharehide", "display_name": "Test", "avatar_url": ""}
        pushes = [{"month": "2026-02", "cost": 100.0, "total_tokens": 5000, "sessions": 3,
                    "active_days": 5, "streak": 2, "payload": "{}"}]
        html = _render_profile_html(user, pushes, {"hideCost": True})
        # Share text should NOT contain cost
        self.assertNotIn("$100", html.split("intent/tweet")[1].split('"')[0])

    def test_leaderboard_share_button(self):
        entries = [
            {"rank": 1, "username": "alice", "display_name": "Alice", "avatar_url": "", "value": 4200, "percentile": 100},
            {"rank": 2, "username": "bob", "display_name": "Bob", "avatar_url": "", "value": 2100, "percentile": 50},
        ]
        html = _render_leaderboard_html(entries, 2, "cost", "current", "February 2026")
        self.assertIn("Share Leaderboard", html)
        self.assertIn("intent/tweet", html)
        self.assertIn("alice", html)

    def test_leaderboard_share_text_format(self):
        entries = [
            {"rank": 1, "username": "alice", "display_name": "Alice", "avatar_url": "", "value": 4200, "percentile": 100},
        ]
        html = _render_leaderboard_html(entries, 1, "cost", "current", "February 2026")
        # Share text should include medal + username
        self.assertIn("MyBurn", html)


class TestCompareRoutes(unittest.TestCase):
    """Integration tests for /compare and /api/compare endpoints."""

    @classmethod
    def setUpClass(cls):
        _init_cloud_db()

        from fastapi import FastAPI
        from fastapi.testclient import TestClient

        cls.test_app = FastAPI()
        register_cloud_routes(cls.test_app)
        cls.client = TestClient(cls.test_app, follow_redirects=False)

        conn = _get_cloud_db()
        cls.user1 = _create_test_user(conn, "cmp_alice", 60001)
        cls.user2 = _create_test_user(conn, "cmp_bob", 60002)
        conn.close()

        # Push data for both users
        cls.client_follow = TestClient(cls.test_app)
        cls.client_follow.post(
            "/api/push",
            json=_make_valid_payload(cost=4200.0),
            headers={"Authorization": f"Bearer {cls.user1['api_token']}"},
        )
        cls.client_follow.post(
            "/api/push",
            json=_make_valid_payload(cost=2100.0),
            headers={"Authorization": f"Bearer {cls.user2['api_token']}"},
        )

    @classmethod
    def tearDownClass(cls):
        conn = _get_cloud_db()
        conn.execute("DELETE FROM pushes WHERE user_id IN (SELECT id FROM users WHERE username IN ('cmp_alice', 'cmp_bob'))")
        conn.execute("DELETE FROM users WHERE username IN ('cmp_alice', 'cmp_bob')")
        conn.commit()
        conn.close()

    def test_html_compare_200(self):
        resp = self.client_follow.get("/compare/cmp_alice/cmp_bob")
        self.assertEqual(resp.status_code, 200)
        self.assertIn("text/html", resp.headers["content-type"])
        self.assertIn("@cmp_alice", resp.text)
        self.assertIn("@cmp_bob", resp.text)
        self.assertIn("VS", resp.text)

    def test_api_compare_200(self):
        resp = self.client_follow.get("/api/compare/cmp_alice/cmp_bob")
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["user1"]["username"], "cmp_alice")
        self.assertEqual(data["user2"]["username"], "cmp_bob")
        self.assertIn("verdict", data)

    def test_url_normalization_redirect(self):
        """Non-alphabetical order should 307 redirect."""
        resp = self.client.get("/compare/cmp_bob/cmp_alice")
        self.assertEqual(resp.status_code, 307)
        self.assertIn("/compare/cmp_alice/cmp_bob", resp.headers["location"])

    def test_api_url_normalization(self):
        resp = self.client.get("/api/compare/cmp_bob/cmp_alice")
        self.assertEqual(resp.status_code, 307)
        self.assertIn("/api/compare/cmp_alice/cmp_bob", resp.headers["location"])

    def test_challenge_mode(self):
        """User2 not found should render challenge page."""
        resp = self.client_follow.get("/compare/cmp_alice/nonexistent_user_xyz")
        self.assertEqual(resp.status_code, 200)
        self.assertIn("challenged you", resp.text)
        self.assertIn("pip install myburn", resp.text)

    def test_api_challenge_mode(self):
        resp = self.client_follow.get("/api/compare/cmp_alice/nonexistent_user_xyz")
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertTrue(data["challenge"])

    def test_user1_not_found_404(self):
        """Both users not found: first looked-up user (alphabetical) returns 404."""
        resp = self.client_follow.get("/compare/aaa_nonexistent/aab_nonexistent")
        self.assertEqual(resp.status_code, 404)

    def test_compare_og_tags(self):
        resp = self.client_follow.get("/compare/cmp_alice/cmp_bob")
        self.assertIn("og:title", resp.text)
        self.assertIn("Who Burned More", resp.text)

    def test_compare_privacy_no_leak(self):
        """If user1 has hideCost, compare should not show cost delta."""
        # Set privacy for user1
        self.client_follow.post(
            "/api/privacy",
            json={"hideCost": True},
            headers={"Authorization": f"Bearer {self.user1['api_token']}"},
        )
        resp = self.client_follow.get("/compare/cmp_alice/cmp_bob")
        self.assertIn("Hidden", resp.text)
        # No ratio leak
        self.assertNotIn("2.0x", resp.text)

        # Clean up: remove privacy
        self.client_follow.post(
            "/api/privacy",
            json={"hideCost": False},
            headers={"Authorization": f"Bearer {self.user1['api_token']}"},
        )


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