import json

import pytest
from django.test import Client, RequestFactory, override_settings

from apps.analytics.models import Hit, Site
from apps.analytics.utils import client_ip
from apps.analytics.views import _aggregate_hits

H = {"HTTP_HOST": "localhost"}


@pytest.fixture
def site(db):
    return Site.objects.create(name="Demo", domain="example.com")


def _post(client, body):
    return client.post("/api/analytics/collect", data=json.dumps(body), content_type="text/plain", **H)


@pytest.mark.django_db
class TestCollectDomainValidation:
    def test_matching_host_recorded(self, site):
        _post(Client(), {"k": site.public_key, "t": "pageview", "u": "https://example.com/a"})
        assert Hit.objects.filter(site=site).count() == 1

    def test_subdomain_allowed(self, site):
        _post(Client(), {"k": site.public_key, "t": "pageview", "u": "https://www.example.com/a"})
        assert Hit.objects.filter(site=site).count() == 1

    def test_foreign_host_rejected(self, site):
        r = _post(Client(), {"k": site.public_key, "t": "pageview", "u": "https://evil.com/a"})
        assert r.status_code == 200  # endpoint still 200 (fire-and-forget)
        assert Hit.objects.filter(site=site).count() == 0

    def test_unknown_key_rejected(self, db):
        _post(Client(), {"k": "khub-nope", "t": "pageview", "u": "https://example.com/a"})
        assert Hit.objects.count() == 0


@pytest.mark.django_db
class TestPropsBounds:
    def test_nested_dropped_scalars_kept(self, site):
        _post(Client(), {
            "k": site.public_key, "t": "event", "n": "signup",
            "u": "https://example.com/x",
            "p": {"plan": "pro", "qty": 3, "nested": {"a": 1}, "arr": [1, 2]},
        })
        hit = Hit.objects.get(site=site, event_type="event")
        assert hit.props == {"plan": "pro", "qty": 3}

    def test_long_value_truncated(self, site):
        _post(Client(), {
            "k": site.public_key, "t": "event", "n": "big",
            "u": "https://example.com/x",
            "p": {"blob": "z" * 5000},
        })
        hit = Hit.objects.get(site=site, event_type="event")
        assert len(hit.props["blob"]) == 256  # per-value cap

    def test_total_size_over_cap_dropped(self, site):
        # 20 max-length string values exceed the total serialized-byte cap → all dropped.
        _post(Client(), {
            "k": site.public_key, "t": "event", "n": "huge",
            "u": "https://example.com/x",
            "p": {f"k{i}": "z" * 5000 for i in range(20)},
        })
        hit = Hit.objects.get(site=site, event_type="event")
        assert hit.props == {}

    def test_key_count_capped(self, site):
        _post(Client(), {
            "k": site.public_key, "t": "event", "n": "many",
            "u": "https://example.com/x",
            "p": {f"k{i}": i for i in range(50)},
        })
        hit = Hit.objects.get(site=site, event_type="event")
        assert len(hit.props) <= 20


@pytest.mark.django_db
class TestSessionMetrics:
    def test_three_pageviews_one_session(self, site):
        for path in ("a", "b", "c"):
            _post(Client(), {
                "k": site.public_key, "t": "pageview",
                "sid": "sess-1", "u": f"https://example.com/{path}",
            })
        assert Hit.objects.filter(site=site, session_id="sess-1").count() == 3

        stats = _aggregate_hits(site.hits.all())
        assert stats["total_pageviews"] == 3
        assert stats["sessions"] == 1
        assert stats["pages_per_session"] == 3.0
        assert stats["bounce_rate"] == 0

    def test_single_pageview_bounces(self, site):
        _post(Client(), {
            "k": site.public_key, "t": "pageview",
            "sid": "sess-2", "u": "https://example.com/x",
        })
        stats = _aggregate_hits(site.hits.all())
        assert stats["sessions"] == 1
        assert stats["bounce_rate"] == 100


class TestClientIp:
    def setup_method(self):
        self.rf = RequestFactory()

    @override_settings(TRUSTED_PROXY_COUNT=0)
    def test_xff_ignored_without_trusted_proxy(self):
        req = self.rf.get("/", HTTP_X_FORWARDED_FOR="1.2.3.4", REMOTE_ADDR="10.0.0.1")
        assert client_ip(req) == "10.0.0.1"

    @override_settings(TRUSTED_PROXY_COUNT=1)
    def test_one_trusted_proxy_uses_client_entry(self):
        # XFF = "<client>, <edge>"; with 1 trusted hop the real client is the left entry.
        req = self.rf.get("/", HTTP_X_FORWARDED_FOR="9.9.9.9, 10.0.0.1", REMOTE_ADDR="10.0.0.1")
        assert client_ip(req) == "9.9.9.9"

    @override_settings(TRUSTED_PROXY_COUNT=1)
    def test_spoof_with_too_few_hops_falls_back(self):
        req = self.rf.get("/", HTTP_X_FORWARDED_FOR="9.9.9.9", REMOTE_ADDR="10.0.0.1")
        assert client_ip(req) == "10.0.0.1"
