"""Tests for autodebug_replay/http_replay.py."""

from __future__ import annotations

import json

import pytest

from autodebug_replay.canonicalize import body_hash, canonical_url
from autodebug_replay.http_replay import GuardedHeaders, _decode_body, _expect_http, _normalize_volatile
from autodebug_replay.transcripts import ReplayDivergence, TranscriptIndex


def _make_http_record(
    seq: int,
    method: str = "GET",
    url: str = "https://example.com/path",
    body: bytes = b"",
    content_type: str | None = None,
    status: int = 200,
    response_body: str = "ok",
    request_body_bytes: str | None = None,
) -> dict:
    """Build a transcript HTTP record that _expect_http will match against."""
    record = {
        "type": "HTTP",
        "seq": seq,
        "method": method,
        "url_canonical": canonical_url(url),
        "body_hash": body_hash(body, content_type),
        "status": status,
        "response_body_bytes": response_body,
        "response_body_encoding": "utf-8",
        "headers_canon": {"content-type": "text/plain"},
    }
    if request_body_bytes is not None:
        record["request_body_bytes"] = request_body_bytes
        record["request_body_encoding"] = "utf-8"
    return record


def _make_index(records: list[dict]) -> TranscriptIndex:
    return TranscriptIndex(records=records)


# ---------------------------------------------------------------------------
# _expect_http
# ---------------------------------------------------------------------------


class TestExpectHttpMatchesCorrectly:
    """test_expect_http_matches_correctly - correct method/url/body matches."""

    def test_get_with_empty_body(self):
        record = _make_http_record(seq=0)
        index = _make_index([record])
        result = _expect_http(index, "GET", "https://example.com/path", b"", None)
        assert result["seq"] == 0
        assert result["status"] == 200

    def test_post_with_json_body(self):
        body = json.dumps({"key": "value"}).encode("utf-8")
        ct = "application/json"
        record = _make_http_record(seq=1, method="POST", body=body, content_type=ct)
        index = _make_index([record])
        result = _expect_http(index, "POST", "https://example.com/path", body, ct)
        assert result["seq"] == 1

    def test_url_with_query_params_reordered(self):
        """Query params are sorted by canonical_url, so order doesn't matter."""
        url = "https://example.com/path?b=2&a=1"
        record = _make_http_record(seq=2, url=url)
        index = _make_index([record])
        # Pass the same URL with different query order
        result = _expect_http(index, "GET", "https://example.com/path?a=1&b=2", b"", None)
        assert result["seq"] == 2


class TestMethodMismatchRaises:
    """test_method_mismatch_raises - wrong method raises MISMATCH."""

    def test_get_vs_post(self):
        record = _make_http_record(seq=0, method="GET")
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "POST", "https://example.com/path", b"", None)
        assert exc_info.value.code == "MISMATCH"
        assert exc_info.value.detail["expected_method"] == "GET"
        assert exc_info.value.detail["actual_method"] == "POST"

    def test_put_vs_patch(self):
        record = _make_http_record(seq=0, method="PUT")
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "PATCH", "https://example.com/path", b"", None)
        assert exc_info.value.code == "MISMATCH"

    def test_delete_vs_get(self):
        record = _make_http_record(seq=0, method="DELETE")
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "GET", "https://example.com/path", b"", None)
        assert exc_info.value.code == "MISMATCH"


class TestUrlMismatchRaises:
    """test_url_mismatch_raises - wrong URL raises MISMATCH."""

    def test_different_path(self):
        record = _make_http_record(seq=0, url="https://example.com/path")
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "GET", "https://example.com/other", b"", None)
        assert exc_info.value.code == "MISMATCH"
        assert "expected_url" in exc_info.value.detail
        assert "actual_url" in exc_info.value.detail

    def test_different_host(self):
        record = _make_http_record(seq=0, url="https://api.one.com/v1")
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "GET", "https://api.two.com/v1", b"", None)
        assert exc_info.value.code == "MISMATCH"


class TestBodyHashMismatchRaisesPayloadMismatch:
    """test_body_hash_mismatch_raises_payload_mismatch - wrong body raises PAYLOAD_MISMATCH."""

    def test_different_json_body(self):
        recorded_body = json.dumps({"a": 1}).encode("utf-8")
        actual_body = json.dumps({"b": 2}).encode("utf-8")
        ct = "application/json"
        record = _make_http_record(seq=0, method="POST", body=recorded_body, content_type=ct)
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "POST", "https://example.com/path", actual_body, ct)
        assert exc_info.value.code == "PAYLOAD_MISMATCH"
        assert exc_info.value.detail["expected_body_hash"] != exc_info.value.detail["actual_body_hash"]

    def test_different_raw_body(self):
        ct = "text/plain"
        record = _make_http_record(seq=0, method="POST", body=b"hello", content_type=ct)
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "POST", "https://example.com/path", b"world", ct)
        assert exc_info.value.code == "PAYLOAD_MISMATCH"

    def test_payload_mismatch_includes_preview(self):
        ct = "application/json"
        recorded_body = json.dumps({"x": 10}).encode("utf-8")
        actual_body = json.dumps({"y": 20}).encode("utf-8")
        record = _make_http_record(seq=0, method="POST", body=recorded_body, content_type=ct)
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "POST", "https://example.com/path", actual_body, ct)
        assert "actual_body_preview" in exc_info.value.detail

    def test_payload_semantic_match_ignores_uuid_timestamp_fields(self):
        ct = "application/json"
        recorded = {"id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2024-01-01T10:00:00Z", "name": "a"}
        actual = {"id": "123e4567-e89b-12d3-a456-426614174000", "created_at": "2026-02-12T11:22:33Z", "name": "a"}
        recorded_body = json.dumps(recorded).encode("utf-8")
        actual_body = json.dumps(actual).encode("utf-8")
        record = _make_http_record(
            seq=0,
            method="POST",
            body=recorded_body,
            content_type=ct,
            request_body_bytes=recorded_body.decode("utf-8"),
        )
        index = _make_index([record])
        result = _expect_http(index, "POST", "https://example.com/path", actual_body, ct)
        assert result["seq"] == 0

    def test_payload_mismatch_reports_structured_fields(self):
        ct = "application/json"
        recorded = {"name": "alice", "profile": {"city": "SF", "active": True}}
        actual = {"name": "bob", "profile": {"city": "NYC", "active": True}}
        recorded_body = json.dumps(recorded).encode("utf-8")
        actual_body = json.dumps(actual).encode("utf-8")
        record = _make_http_record(
            seq=0,
            method="POST",
            body=recorded_body,
            content_type=ct,
            request_body_bytes=recorded_body.decode("utf-8"),
        )
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "POST", "https://example.com/path", actual_body, ct)
        assert exc_info.value.code == "PAYLOAD_MISMATCH"
        mismatches = exc_info.value.detail.get("field_mismatches", [])
        assert "name" in mismatches or "profile.city" in mismatches
        assert exc_info.value.detail.get("mismatch_count", 0) >= 1

    def test_payload_match_when_recorded_hash_is_short_prefix(self):
        """Backward compatibility: old transcripts may store short body hashes."""
        ct = "application/json"
        body = json.dumps({"ok": True, "n": 1}).encode("utf-8")
        record = _make_http_record(seq=0, method="POST", body=body, content_type=ct)
        record["body_hash"] = record["body_hash"][:16]  # legacy short hash format
        index = _make_index([record])

        result = _expect_http(index, "POST", "https://example.com/path", body, ct)
        assert result["seq"] == 0

    def test_payload_mismatch_marks_volatile_only_false_for_real_diff(self):
        ct = "application/json"
        recorded = {"name": "alice", "created_at": "2024-01-01T00:00:00Z"}
        actual = {"name": "bob", "created_at": "2026-02-12T11:22:33Z"}
        recorded_body = json.dumps(recorded).encode("utf-8")
        actual_body = json.dumps(actual).encode("utf-8")
        record = _make_http_record(
            seq=0,
            method="POST",
            body=recorded_body,
            content_type=ct,
            request_body_bytes=recorded_body.decode("utf-8"),
        )
        index = _make_index([record])
        with pytest.raises(ReplayDivergence) as exc_info:
            _expect_http(index, "POST", "https://example.com/path", actual_body, ct)
        assert exc_info.value.detail.get("volatile_only_mismatch") is False


# ---------------------------------------------------------------------------
# _normalize_volatile
# ---------------------------------------------------------------------------


class TestNormalizeVolatileStripsUuids:
    """test_normalize_volatile_strips_uuids - UUID pattern replaced with <UUID>."""

    def test_single_uuid(self):
        body = json.dumps({"id": "550e8400-e29b-41d4-a716-446655440000"}).encode("utf-8")
        result = _normalize_volatile(body, "application/json")
        parsed = json.loads(result)
        assert parsed["id"] == "<UUID>"

    def test_multiple_uuids(self):
        body = json.dumps({
            "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            "session_id": "12345678-1234-1234-1234-123456789abc",
        }).encode("utf-8")
        result = _normalize_volatile(body, "application/json")
        parsed = json.loads(result)
        assert parsed["user_id"] == "<UUID>"
        assert parsed["session_id"] == "<UUID>"

    def test_preserves_non_uuid_values(self):
        body = json.dumps({"name": "alice", "id": "550e8400-e29b-41d4-a716-446655440000"}).encode("utf-8")
        result = _normalize_volatile(body, "application/json")
        parsed = json.loads(result)
        assert parsed["name"] == "alice"
        assert parsed["id"] == "<UUID>"


class TestNormalizeVolatileStripsTimestamps:
    """test_normalize_volatile_strips_timestamps - ISO timestamp replaced with <TIMESTAMP>."""

    def test_iso_timestamp_with_z(self):
        body = json.dumps({"created_at": "2024-01-15T10:30:00Z"}).encode("utf-8")
        result = _normalize_volatile(body, "application/json")
        parsed = json.loads(result)
        assert parsed["created_at"] == "<TIMESTAMP>"

    def test_iso_timestamp_with_offset(self):
        body = json.dumps({"ts": "2024-01-15T10:30:00+05:30"}).encode("utf-8")
        result = _normalize_volatile(body, "application/json")
        parsed = json.loads(result)
        assert parsed["ts"] == "<TIMESTAMP>"

    def test_iso_timestamp_with_microseconds(self):
        body = json.dumps({"when": "2024-01-15T10:30:00.123456"}).encode("utf-8")
        result = _normalize_volatile(body, "application/json")
        parsed = json.loads(result)
        assert parsed["when"] == "<TIMESTAMP>"

    def test_epoch_timestamps_stripped(self):
        """Epoch timestamps (10+ digits) are replaced with <EPOCH>."""
        body = json.dumps({"epoch": "1705312200"}).encode("utf-8")
        result = _normalize_volatile(body, "application/json")
        parsed = json.loads(result)
        assert parsed["epoch"] == "<EPOCH>"


class TestNormalizeVolatileIgnoresNonJson:
    """test_normalize_volatile_ignores_non_json - non-JSON content_type returns body unchanged."""

    def test_text_plain(self):
        body = b'{"id": "550e8400-e29b-41d4-a716-446655440000"}'
        result = _normalize_volatile(body, "text/plain")
        assert result == body

    def test_octet_stream(self):
        body = b"\x00\x01\x02"
        result = _normalize_volatile(body, "application/octet-stream")
        assert result == body

    def test_none_content_type(self):
        body = b'{"id": "550e8400-e29b-41d4-a716-446655440000"}'
        result = _normalize_volatile(body, None)
        assert result == body

    def test_empty_content_type(self):
        body = b'{"id": "550e8400-e29b-41d4-a716-446655440000"}'
        result = _normalize_volatile(body, "")
        assert result == body

    def test_xml_content_type(self):
        body = b"<root><id>550e8400-e29b-41d4-a716-446655440000</id></root>"
        result = _normalize_volatile(body, "application/xml")
        assert result == body


class TestHeaderAndPayloadHardening:
    def test_guarded_headers_case_insensitive_lookup(self):
        headers = GuardedHeaders({"Content-Type": "application/json", "X-Trace": "abc"})
        assert headers["content-type"] == "application/json"
        assert headers["CONTENT-TYPE"] == "application/json"
        assert headers.get("x-trace") == "abc"

    def test_decode_body_invalid_base64_raises_structured_divergence(self):
        with pytest.raises(ReplayDivergence) as exc_info:
            _decode_body("!!!not-base64!!!", "base64")
        assert exc_info.value.code == "INVALID_RECORDED_PAYLOAD"
