"""Tests for HAR sanitization utilities.

This module tests sanitization of HTTP Archive (HAR) format files,
ensuring sensitive data is redacted while preserving debugging utility.

Test Coverage:
    - Sensitive field detection in various HAR sections
    - Header sanitization (Authorization, Cookie, Set-Cookie)
    - POST data sanitization (form fields, JSON payloads)
    - Entry-level sanitization (requests and responses)
    - Full HAR file sanitization end-to-end
    - Correlation-preserving redaction with salted hashes

Test Strategy:
    - Unit tests for each sanitization layer
    - Integration tests for complete HAR processing
    - Validation that non-sensitive data is preserved
    - Hash consistency verification across values

Dependencies:
    - pytest for test framework
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from har_capture.sanitization.har import (
    HarValidationError,
    _embed_sanitization_metadata,
    _sanitize_form_urlencoded,
    _sanitize_json_recursive,
    _sanitize_string_patterns,
    apply_user_redactions,
    is_flaggable_field,
    is_sensitive_field,
    sanitize_entry,
    sanitize_har,
    sanitize_har_file,
    sanitize_header_value,
    sanitize_post_data,
    validate_har_structure,
)
from har_capture.sanitization.report import (
    FlaggedValue,
    HeuristicMode,
    RedactionStatus,
    SanitizationReport,
)

# =============================================================================
# Fixture Loading
# =============================================================================

_FIXTURES_DIR = Path(__file__).resolve().parent.parent / "fixtures"


def _load_fixture(name: str) -> dict:
    """Load a JSON fixture file and return the parsed dict."""
    with open(_FIXTURES_DIR / name) as f:
        return json.load(f)


_HAR_FIXTURE = _load_fixture("test_har.json")

# =============================================================================
# Test Data Tables -- large tables loaded from tests/fixtures/test_har.json
# =============================================================================

# Sensitive field detection: (field_name, expected, id)
SENSITIVE_FIELD_CASES = [
    (c["field_name"], c["expected"], c["id"]) for c in _HAR_FIXTURE["sensitive_field_cases"]
]

# Header redaction: (header_name, header_value, expected_contains, id)
HEADER_REDACTION_CASES = [
    (c["header_name"], c["header_value"], c["expected_contains"], c["id"])
    for c in _HAR_FIXTURE["header_redaction_cases"]
]

# ┌─────────────────────────┬─────────────────────────────┬─────────────────────┐
# │ header_name             │ header_value                │ description         │
# ├─────────────────────────┼─────────────────────────────┼─────────────────────┤
# │ HTTP header name        │ Value that should be gone   │ test case name      │
# └─────────────────────────┴─────────────────────────────┴─────────────────────┘
#
# fmt: off
HEADER_VALUE_REMOVED_CASES = [
    ("Authorization",       "Bearer abc123xyz",           "auth_bearer_removed"),
    ("Cookie",              "session=abc123",             "cookie_value_removed"),
    ("Set-Cookie",          "session=xyz789; Path=/",     "set_cookie_value_removed"),
]
# fmt: on


# =============================================================================
# Test Classes
# =============================================================================


class TestSensitiveFieldDetection:
    """Tests for sensitive field detection."""

    @pytest.mark.parametrize(
        ("field_name", "expected", "desc"),
        SENSITIVE_FIELD_CASES,
        ids=[c[2] for c in SENSITIVE_FIELD_CASES],
    )
    def test_sensitive_field_detection(self, field_name: str, expected: bool, desc: str) -> None:
        """Test detection of sensitive vs safe field names."""
        result = is_sensitive_field(field_name)
        assert result is expected, f"{desc}: '{field_name}' should be {'sensitive' if expected else 'safe'}"


class TestHeaderSanitization:
    """Tests for header value sanitization."""

    @pytest.mark.parametrize(
        ("name", "value", "expected_contains", "desc"),
        HEADER_REDACTION_CASES,
        ids=[c[3] for c in HEADER_REDACTION_CASES],
    )
    def test_header_sanitization(self, name: str, value: str, expected_contains: str, desc: str) -> None:
        """Test header value sanitization."""
        result = sanitize_header_value(name, value)
        assert expected_contains in result, f"{desc}: result should contain '{expected_contains}'"

    @pytest.mark.parametrize(
        ("name", "value", "desc"),
        HEADER_VALUE_REMOVED_CASES,
        ids=[c[2] for c in HEADER_VALUE_REMOVED_CASES],
    )
    def test_sensitive_value_removed(self, name: str, value: str, desc: str) -> None:
        """Test sensitive header values are removed."""
        result = sanitize_header_value(name, value)
        # Extract the actual secret part (after = for cookies, whole value for auth)
        if "=" in value:
            secret = value.split("=", maxsplit=1)[1].split(";", maxsplit=1)[0]
        else:
            secret = value.rsplit(" ", maxsplit=1)[-1] if " " in value else value
        assert secret not in result, f"{desc}: secret '{secret}' should be removed"


class TestPostDataSanitization:
    """Tests for POST data sanitization."""

    # fmt: off
    POST_PARAM_CASES = [
        # (field_name, field_value, should_redact, description)
        ("loginPassword",   "secret123",    True,   "password_redacted"),
        ("userPassword",    "mypass",       True,   "user_password_redacted"),
        ("auth_token",      "tok123",       True,   "auth_token_redacted"),
        ("loginName",       "admin",        False,  "login_name_flagged_not_redacted"),
        ("email",           "a@b.com",      False,  "email_preserved"),
        ("channel",         "123",          False,  "channel_preserved"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("field_name", "field_value", "should_redact", "desc"),
        POST_PARAM_CASES,
        ids=[c[3] for c in POST_PARAM_CASES],
    )
    def test_post_param_sanitization(
        self, field_name: str, field_value: str, should_redact: bool, desc: str
    ) -> None:
        """Test POST parameter sanitization."""
        post_data = {
            "mimeType": "application/x-www-form-urlencoded",
            "params": [{"name": field_name, "value": field_value}],
        }
        result = sanitize_post_data(post_data)
        assert result is not None
        result_value = result["params"][0]["value"]

        if should_redact:
            assert result_value == "[REDACTED]", f"{desc}: value should be redacted"
        else:
            assert result_value == field_value, f"{desc}: value should be preserved"

    def test_sanitizes_password_in_text(self) -> None:
        """Test password in text redaction."""
        post_data = {
            "mimeType": "application/x-www-form-urlencoded",
            "text": "fieldName=admin&loginPassword=secret123",
        }
        result = sanitize_post_data(post_data)
        assert result is not None
        assert "fieldName=admin" in result["text"]
        assert "loginPassword=[REDACTED]" in result["text"]
        assert "secret123" not in result["text"]

    def test_sanitizes_json_post_data(self) -> None:
        """Test JSON password redaction."""
        post_data = {
            "mimeType": "application/json",
            "text": '{"displayName": "admin", "password": "secret123"}',
        }
        result = sanitize_post_data(post_data)
        assert result is not None
        parsed = json.loads(result["text"])
        assert parsed["displayName"] == "admin"
        assert parsed["password"] == "[REDACTED]"

    def test_sanitizes_nested_json(self) -> None:
        """Test nested JSON password redaction with recursive handling."""
        post_data = {
            "mimeType": "application/json",
            "text": '{"displayName": "admin", "password": "secret"}',
        }
        result = sanitize_post_data(post_data)
        assert result is not None
        parsed = json.loads(result["text"])
        assert parsed["displayName"] == "admin"
        assert parsed["password"] == "[REDACTED]"

    # fmt: off
    EDGE_CASES = [
        (None,  None,   "none_returns_none"),
        ({},    {},     "empty_returns_empty"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("input_data", "expected", "desc"),
        EDGE_CASES,
        ids=[c[2] for c in EDGE_CASES],
    )
    def test_edge_cases(self, input_data, expected, desc: str) -> None:
        """Test edge cases."""
        result = sanitize_post_data(input_data)
        assert result == expected, f"{desc}"


class TestEntrySanitization:
    """Tests for full HAR entry sanitization."""

    def test_sanitizes_request_headers(self) -> None:
        """Test request header sanitization."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://example.com/",
                "headers": [
                    {"name": "Cookie", "value": "session=secret123"},
                    {"name": "Content-Type", "value": "text/html"},
                ],
            },
            "response": {
                "status": 200,
                "headers": [],
                "content": {"text": "", "mimeType": "text/html"},
            },
        }
        result = sanitize_entry(entry, salt=None)
        cookie_header = next(h for h in result["request"]["headers"] if h["name"] == "Cookie")
        assert "secret123" not in cookie_header["value"]
        # Cookie values are redacted with ***COOKIE*** placeholder
        assert "***COOKIE***" in cookie_header["value"] or "[REDACTED]" in cookie_header["value"]

    def test_sanitizes_response_headers(self) -> None:
        """Test response header sanitization."""
        entry = {
            "request": {"method": "GET", "url": "http://test/", "headers": []},
            "response": {
                "status": 200,
                "headers": [{"name": "Set-Cookie", "value": "token=abc123; HttpOnly"}],
                "content": {"text": "", "mimeType": "text/html"},
            },
        }
        result = sanitize_entry(entry)
        cookie_header = next(h for h in result["response"]["headers"] if h["name"] == "Set-Cookie")
        assert "abc123" not in cookie_header["value"]

    def test_sanitizes_response_content(self) -> None:
        """Test response content sanitization."""
        entry = {
            "request": {"method": "GET", "url": "http://example.com/", "headers": []},
            "response": {
                "status": 200,
                "headers": [],
                "content": {
                    "text": "<html>MAC: AA:BB:CC:DD:EE:FF</html>",
                    "mimeType": "text/html",
                },
            },
        }
        result = sanitize_entry(entry, salt=None)
        content = result["response"]["content"]["text"]
        assert "AA:BB:CC:DD:EE:FF" not in content
        assert "XX:XX:XX:XX:XX:XX" in content

    def test_sanitizes_query_string(self) -> None:
        """Test query string parameter sanitization."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://test/?password=secret",
                "headers": [],
                "queryString": [{"name": "password", "value": "secret"}],
            },
            "response": {"status": 200, "headers": [], "content": {}},
        }
        result = sanitize_entry(entry, salt=None)
        password_param = next(p for p in result["request"]["queryString"] if p["name"] == "password")
        # Password values are redacted with ***FIELD*** placeholder (salt=None)
        assert password_param["value"] in ("[REDACTED]", "***FIELD***")


class TestFullHarSanitization:
    """Tests for complete HAR sanitization."""

    def test_sanitizes_all_entries(self) -> None:
        """Test all entries are sanitized."""
        har_data = {
            "log": {
                "version": "1.2",
                "entries": [
                    {
                        "request": {
                            "method": "GET",
                            "url": "http://example.com/",
                            "headers": [{"name": "Cookie", "value": "session=abc"}],
                        },
                        "response": {
                            "status": 200,
                            "headers": [],
                            "content": {"text": "MAC: 11:22:33:44:55:66", "mimeType": "text/html"},
                        },
                    },
                ],
            }
        }
        result, _ = sanitize_har(har_data, salt=None)
        entry = result["log"]["entries"][0]
        assert "11:22:33:44:55:66" not in entry["response"]["content"]["text"]

    def test_sanitizes_multiple_entries(self) -> None:
        """Test multiple entries are all sanitized."""
        har_data = {
            "log": {
                "version": "1.2",
                "entries": [
                    {
                        "request": {"method": "GET", "url": "http://test/1", "headers": []},
                        "response": {
                            "status": 200,
                            "headers": [],
                            "content": {"text": "MAC: AA:AA:AA:AA:AA:AA", "mimeType": "text/html"},
                        },
                    },
                    {
                        "request": {"method": "GET", "url": "http://test/2", "headers": []},
                        "response": {
                            "status": 200,
                            "headers": [],
                            "content": {"text": "MAC: BB:BB:BB:BB:BB:BB", "mimeType": "text/html"},
                        },
                    },
                ],
            }
        }
        result, _ = sanitize_har(har_data, salt=None)
        assert "AA:AA:AA:AA:AA:AA" not in result["log"]["entries"][0]["response"]["content"]["text"]
        assert "BB:BB:BB:BB:BB:BB" not in result["log"]["entries"][1]["response"]["content"]["text"]

    def test_handles_missing_log(self) -> None:
        """Test handling of missing log key."""
        har_data = {"invalid": "structure"}
        result, _ = sanitize_har(har_data)
        assert "invalid" in result

    def test_handles_empty_entries(self) -> None:
        """Test handling of empty entries list."""
        har_data = {"log": {"version": "1.2", "entries": []}}
        result, _ = sanitize_har(har_data)
        assert result["log"]["entries"] == []

    def test_preserves_structure(self) -> None:
        """Test HAR structure is preserved."""
        har_data = {
            "log": {
                "version": "1.2",
                "creator": {"name": "Test", "version": "1.0"},
                "entries": [],
                "pages": [{"title": "Test Page"}],
            }
        }
        result, _ = sanitize_har(har_data)
        assert result["log"]["version"] == "1.2"
        assert result["log"]["creator"]["name"] == "Test"
        assert len(result["log"]["pages"]) == 1

    def test_sanitizes_page_titles(self) -> None:
        """Test page titles are sanitized."""
        har_data = {
            "log": {
                "version": "1.2",
                "entries": [],
                "pages": [{"title": "Device MAC: AA:BB:CC:DD:EE:FF"}],
            }
        }
        result, _ = sanitize_har(har_data, salt=None)
        assert "AA:BB:CC:DD:EE:FF" not in result["log"]["pages"][0]["title"]


class TestURLStringSanitization:
    """Tests for URL string query parameter sanitization."""

    # fmt: off
    URL_SANITIZATION_CASES = [
        # (url, should_redact_value, desc)
        ("http://example.com/login?access_token=secret123&page=1", "secret123", "access_token_in_url"),
        ("http://example.com/api?password=hunter2&format=json", "hunter2", "password_in_url"),
        ("http://example.com/data?page=1&limit=10", None, "no_sensitive_params"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("url", "should_redact_value", "desc"),
        URL_SANITIZATION_CASES,
        ids=[c[2] for c in URL_SANITIZATION_CASES],
    )
    def test_url_query_param_sanitization(self, url: str, should_redact_value: str | None, desc: str) -> None:
        """Test sensitive query parameters are redacted in URL string."""
        entry = {
            "request": {"method": "GET", "url": url, "headers": [], "queryString": []},
            "response": {"status": 200, "headers": [], "content": {}},
        }
        result = sanitize_entry(entry, salt=None)
        result_url = result["request"]["url"]
        if should_redact_value:
            assert should_redact_value not in result_url, f"{desc}: value should be redacted from URL"
        else:
            assert result_url == url, f"{desc}: URL should be unchanged"


class TestURLPathSanitization:
    """Tests for URL path segment sanitization (flagged, not auto-redacted)."""

    # fmt: off
    URL_PATH_CASES = [
        ("http://api.example.com/users/550e8400-e29b-41d4-a716-446655440000/profile", "uuid_in_path"),
        ("http://api.example.com/keys/sk-1234567890abcdefghij/verify", "api_key_prefix_in_path"),
        ("http://api.example.com/tokens/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6/refresh", "long_mixed_token_in_path"),
        ("http://api.example.com/devices/DEV-ABC123456/status", "device_serial_in_path"),
        ("http://api.example.com/customers/CUST-67890/profile", "customer_id_in_path"),
        ("http://example.com/api/v1/status", "normal_path_preserved"),
        ("http://example.com/api/GetDeviceInformation/details", "long_alpha_path_preserved"),
        ("http://example.com/api/configurationSettings/list", "camelcase_path_preserved"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("url", "desc"),
        URL_PATH_CASES,
        ids=[c[1] for c in URL_PATH_CASES],
    )
    def test_url_path_segments_preserved(self, url: str, desc: str) -> None:
        """Test path segments are preserved (flagged for review, not auto-redacted)."""
        entry = {
            "request": {"method": "GET", "url": url, "headers": [], "queryString": []},
            "response": {"status": 200, "headers": [], "content": {}},
        }
        result = sanitize_entry(entry, salt=None)
        result_url = result["request"]["url"]
        assert result_url == url, f"{desc}: URL should be preserved (flagged, not redacted)"


class TestResponseContentFallback:
    """Tests for fallback mime type sanitization."""

    # fmt: off
    FALLBACK_CASES = [
        ("text/plain", "IP: 192.168.1.100", "192.168.1.100", "text_plain_sanitized"),
        ("text/javascript", "var ip='192.168.2.50'", "192.168.2.50", "text_javascript_sanitized"),
        ("", "IP: 192.168.3.50 here", "192.168.3.50", "missing_mime_sanitized"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("mime_type", "content_text", "expected_redacted", "desc"),
        FALLBACK_CASES,
        ids=[c[3] for c in FALLBACK_CASES],
    )
    def test_text_content_fallback_sanitization(
        self, mime_type: str, content_text: str, expected_redacted: str, desc: str
    ) -> None:
        """Test text/* and missing mime type content gets pattern-sanitized."""
        entry = {
            "request": {"method": "GET", "url": "http://test/", "headers": []},
            "response": {
                "status": 200,
                "headers": [],
                "content": {"text": content_text, "mimeType": mime_type},
            },
        }
        result = sanitize_entry(entry, salt=None)
        content = result["response"]["content"]["text"]
        assert expected_redacted not in content, f"{desc}: PII should be redacted"

    def test_base64_content_skipped(self) -> None:
        """Test base64-encoded content is not pattern-sanitized."""
        entry = {
            "request": {"method": "GET", "url": "http://test/", "headers": []},
            "response": {
                "status": 200,
                "headers": [],
                "content": {
                    "text": "MTkyLjE2OC4xLjEwMA==",
                    "mimeType": "application/octet-stream",
                    "encoding": "base64",
                },
            },
        }
        result = sanitize_entry(entry, salt=None)
        assert result["response"]["content"]["text"] == "MTkyLjE2OC4xLjEwMA=="


class TestNestedJSONSanitization:
    """Tests for _sanitize_json_text handling nested objects."""

    def test_nested_sensitive_fields_in_post_data(self) -> None:
        """Test _sanitize_json_text now handles nested sensitive fields."""
        post_data = {
            "mimeType": "application/json",
            "text": '{"data": {"password": "secret", "nested": {"token": "abc123"}}}',
        }
        result = sanitize_post_data(post_data)
        assert result is not None
        parsed = json.loads(result["text"])
        assert parsed["data"]["password"] == "[REDACTED]"
        assert parsed["data"]["nested"]["token"] == "[REDACTED]"


class TestIPValidation:
    """Tests for IP validation in string pattern sanitization."""

    @pytest.mark.parametrize(
        ("input_text", "should_contain", "desc"),
        [
            ("IP: 192.168.999.999", "192.168.999.999", "invalid_octets_preserved"),
            ("IP: 192.168.1.100", None, "valid_ip_redacted"),
        ],
    )
    def test_ip_validation_in_string_patterns(
        self, input_text: str, should_contain: str | None, desc: str
    ) -> None:
        """Test IP validation rejects invalid octets in string patterns."""
        from har_capture.sanitization.har import _sanitize_string_patterns

        result = _sanitize_string_patterns(input_text)
        if should_contain:
            assert should_contain in result, f"{desc}: invalid IP should be preserved"
        else:
            assert "192.168.1.100" not in result, f"{desc}: valid IP should be redacted"


class TestSensitiveFieldPatterns:
    """Tests for tightened sensitive field patterns (over-matching fixes)."""

    # Over-matching prevention: (field_name, expected, id)
    OVER_MATCHING_CASES = [
        (c["field_name"], c["expected"], c["id"]) for c in _HAR_FIXTURE["over_matching_cases"]
    ]

    @pytest.mark.parametrize(
        ("field_name", "expected", "desc"),
        OVER_MATCHING_CASES,
        ids=[c[2] for c in OVER_MATCHING_CASES],
    )
    def test_sensitive_field_pattern_accuracy(self, field_name: str, expected: bool, desc: str) -> None:
        """Test tightened patterns prevent over-matching."""
        assert is_sensitive_field(field_name) == expected, f"{desc}"


class TestSSNAndCreditCardPatterns:
    """Tests for SSN (flagged) and credit card (auto-redacted) pattern detection."""

    @pytest.mark.parametrize(
        ("input_text", "should_redact", "desc"),
        [
            ("SSN: 123-45-6789", False, "ssn_flagged_not_redacted"),
            ("Date: 2024-01-15", False, "date_not_matched"),
            ("Card: 4111111111111111", True, "visa_detected"),
            ("Card: 5500000000000004", True, "mastercard_detected"),
            ("Card: 371449635398431", True, "amex_detected"),
            ("Number: 1234567890123456", False, "random_digits_no_luhn"),
        ],
    )
    def test_financial_pii_detection(self, input_text: str, should_redact: bool, desc: str) -> None:
        """Test credit card auto-redaction and SSN preservation (flagged only)."""
        from har_capture.sanitization.har import _sanitize_string_patterns

        result = _sanitize_string_patterns(input_text)
        if should_redact:
            # Extract the value that should be redacted
            original_numbers = [w for w in input_text.split() if any(c.isdigit() for c in w)]
            for num in original_numbers:
                if len(num) > 8:  # Only check long numbers (CC)
                    assert num not in result, f"{desc}: {num} should be redacted"
        else:
            assert result == input_text, f"{desc}: text should be unchanged"


class TestFlaggableFieldDetection:
    """Tests for is_flaggable_field() — fields flagged for review, not auto-redacted."""

    # Flaggable field detection: (field_name, expected, id)
    FLAGGABLE_FIELD_CASES = [
        (c["field_name"], c["expected"], c["id"]) for c in _HAR_FIXTURE["flaggable_field_cases"]
    ]

    @pytest.mark.parametrize(
        ("field_name", "expected", "desc"),
        FLAGGABLE_FIELD_CASES,
        ids=[c[2] for c in FLAGGABLE_FIELD_CASES],
    )
    def test_flaggable_field_detection(self, field_name: str, expected: bool, desc: str) -> None:
        """Test detection of flaggable vs non-flaggable field names."""
        result = is_flaggable_field(field_name)
        assert result is expected, (
            f"{desc}: '{field_name}' should be {'flaggable' if expected else 'not flaggable'}"
        )


class TestFlaggingBehavior:
    """Tests that flaggable fields, SSNs, and URL paths are flagged (not auto-redacted)."""

    def test_ssn_flagged_not_redacted(self) -> None:
        """Test SSN patterns are flagged for review, not auto-redacted."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_string_patterns

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_string_patterns("SSN: 123-45-6789", collector=collector)
        assert "123-45-6789" in result, "SSN should be preserved in output"
        assert any(f.category == "ssn" for f in collector.flagged), "SSN should be in flagged list"

    def test_url_path_uuid_flagged(self) -> None:
        """Test UUID in URL path is flagged, not redacted."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_url_path

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        url = "http://api.example.com/users/550e8400-e29b-41d4-a716-446655440000/profile"
        result = _sanitize_url_path(url, hasher, collector)
        assert "550e8400-e29b-41d4-a716-446655440000" in result, "UUID should be preserved"
        assert any(f.category == "uuid" for f in collector.flagged), "UUID should be flagged"

    def test_url_path_api_key_flagged(self) -> None:
        """Test API key prefix in URL path is flagged, not redacted."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_url_path

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        url = "http://api.example.com/keys/sk-1234567890abcdefghij/verify"
        result = _sanitize_url_path(url, hasher, collector)
        assert "sk-1234567890abcdefghij" in result, "API key should be preserved"
        assert any(f.category == "api_key" for f in collector.flagged), "API key should be flagged"

    def test_url_path_long_token_flagged(self) -> None:
        """Test long token in URL path is flagged, not redacted."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_url_path

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        url = "http://api.example.com/tokens/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6/refresh"
        result = _sanitize_url_path(url, hasher, collector)
        assert "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" in result, "Token should be preserved"
        assert any(f.category == "token" for f in collector.flagged), "Token should be flagged"

    def test_url_path_device_serial_flagged(self) -> None:
        """Test device/serial number in URL path is flagged, not redacted."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_url_path

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        url = "http://api.example.com/devices/DEV-ABC123456/status"
        result = _sanitize_url_path(url, hasher, collector)
        assert "DEV-ABC123456" in result, "Device serial should be preserved"
        assert any(f.category == "device_serial" for f in collector.flagged), (
            "Device serial should be flagged"
        )

    @pytest.mark.parametrize(
        ("segment", "desc"),
        [
            ("DEV-ABC123456", "device_prefix"),
            ("CUST-67890", "customer_prefix"),
            ("SN-XY12345678", "serial_number_prefix"),
            ("HW-ABCDE12345", "hardware_prefix"),
        ],
        ids=["device_prefix", "customer_prefix", "serial_number_prefix", "hardware_prefix"],
    )
    def test_url_path_device_serial_variants_flagged(self, segment: str, desc: str) -> None:
        """Test various device/serial number patterns are flagged."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_url_path

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        url = f"http://api.example.com/items/{segment}/info"
        result = _sanitize_url_path(url, hasher, collector)
        assert segment in result, f"{desc}: segment should be preserved"
        assert any(f.category == "device_serial" for f in collector.flagged), f"{desc}: should be flagged"

    def test_flaggable_field_in_json_flagged(self) -> None:
        """Test flaggable fields in JSON are flagged, value preserved."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_json_recursive

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        data = {"username": "admin", "password": "secret123"}
        result = _sanitize_json_recursive(data, hasher, collector)
        assert result["username"] == "admin", "Flaggable field value should be preserved"
        assert result["password"] in ("[REDACTED]", "***FIELD***"), "Sensitive field should be auto-redacted"
        assert any(f.original_value == "admin" and f.category == "field" for f in collector.flagged)

    def test_flaggable_field_in_post_params_flagged(self) -> None:
        """Test flaggable fields in POST params are flagged, value preserved."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        post_data = {
            "mimeType": "application/x-www-form-urlencoded",
            "params": [
                {"name": "loginName", "value": "admin"},
                {"name": "password", "value": "secret"},
            ],
        }
        result = sanitize_post_data(post_data, hasher, collector)
        assert result is not None
        login_param = next(p for p in result["params"] if p["name"] == "loginName")
        password_param = next(p for p in result["params"] if p["name"] == "password")
        assert login_param["value"] == "admin", "Flaggable field should be preserved"
        assert password_param["value"] in ("[REDACTED]", "***FIELD***"), "Sensitive field should be redacted"
        assert any(f.original_value == "admin" for f in collector.flagged)

    def test_flaggable_field_in_query_string_flagged(self) -> None:
        """Test flaggable fields in queryString are flagged, value preserved."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_request

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        req = {
            "method": "GET",
            "url": "http://test/?username=admin&password=secret",
            "headers": [],
            "queryString": [
                {"name": "username", "value": "admin"},
                {"name": "password", "value": "secret"},
            ],
        }
        _sanitize_request(req, hasher, collector)
        username_param = next(p for p in req["queryString"] if p["name"] == "username")
        password_param = next(p for p in req["queryString"] if p["name"] == "password")
        assert username_param["value"] == "admin", "Flaggable queryString param should be preserved"
        assert password_param["value"] in ("[REDACTED]", "***FIELD***"), (
            "Sensitive queryString param should be redacted"
        )
        assert any(f.original_value == "admin" for f in collector.flagged)


class TestPhoneNumberPatterns:
    """Tests for phone number pattern detection (flagged, not auto-redacted)."""

    # fmt: off
    PHONE_FLAGGED_CASES = [
        ("Call: (555) 123-4567",            "(555) 123-4567",   "us_parens_format"),
        ("Phone: 555-123-4567",             "555-123-4567",     "us_dash_format"),
        ("Tel: +1 555 123 4567",            "+1 555 123 4567",  "us_intl_format"),
        ("Contact: +1-555-123-4567",        "+1-555-123-4567",  "us_intl_dash_format"),
        ("Fax: 555.123.4567",               "555.123.4567",     "us_dot_format"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("input_text", "expected_phone", "desc"),
        PHONE_FLAGGED_CASES,
        ids=[c[2] for c in PHONE_FLAGGED_CASES],
    )
    def test_phone_flagged_not_redacted(self, input_text: str, expected_phone: str, desc: str) -> None:
        """Test phone number patterns are flagged for review, not auto-redacted."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_string_patterns

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_string_patterns(input_text, collector=collector)
        assert expected_phone in result, f"{desc}: phone number should be preserved in output"
        assert any(f.category == "phone" for f in collector.flagged), f"{desc}: phone should be flagged"

    # fmt: off
    PHONE_NOT_MATCHED_CASES = [
        ("ID: 12345",               "short_number"),
        ("Code: 123-45",            "too_short_with_dash"),
        ("Version: 1.2.3",          "version_number"),
        ("tok_1234567890",          "token_with_digits"),
        ("abc1234567890xyz",        "digits_inside_word"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("input_text", "desc"),
        PHONE_NOT_MATCHED_CASES,
        ids=[c[1] for c in PHONE_NOT_MATCHED_CASES],
    )
    def test_non_phone_not_flagged(self, input_text: str, desc: str) -> None:
        """Test non-phone patterns are not flagged."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_string_patterns

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        _sanitize_string_patterns(input_text, collector=collector)
        assert not any(f.category == "phone" for f in collector.flagged), f"{desc}: should not flag as phone"


# =============================================================================
# Public IP Sanitization in JSON/String Patterns
# =============================================================================


class TestPublicIpSanitization:
    """Tests for public IP address sanitization in string patterns.

    Public IPs should be redacted anywhere they appear in HAR content
    (JSON values, HTML bodies, headers) while private IPs get separate handling.
    """

    # fmt: off
    PUBLIC_IP_REDACTED_CASES = [
        ("73.158.42.197",                 "bare_public_ip"),
        ("WAN IP: 73.158.42.197",        "public_ip_in_text"),
        ('{"wanIp": "73.158.42.197"}',   "public_ip_in_json_string"),
        ("IP=8.8.8.8&dns=1.1.1.1",       "multiple_public_ips"),
        ("203.0.113.42",                  "documentation_range_ip"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("input_text", "desc"),
        PUBLIC_IP_REDACTED_CASES,
        ids=[c[1] for c in PUBLIC_IP_REDACTED_CASES],
    )
    def test_public_ip_redacted(self, input_text: str, desc: str) -> None:
        """Test public IPs are redacted in string patterns."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_string_patterns

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_string_patterns(input_text, collector=collector)
        # Extract the IP from input to verify it's gone
        import re

        ips = re.findall(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", input_text)
        for ip in ips:
            first_octet = int(ip.split(".")[0])
            # Only check non-private IPs
            if not ip.startswith(("10.", "192.168.", "127.")) and first_octet not in (0, 255):
                assert ip not in result, f"{desc}: public IP {ip} should be redacted"

    # fmt: off
    PRIVATE_IP_REDACTED_CASES = [
        ("192.168.5.100",  "private_192_168"),
        ("172.16.0.50",    "private_172_16"),
    ]
    PRIVATE_IP_PRESERVED_CASES = [
        ("192.168.1.1",    "gateway_192_168_1_1"),
        ("10.0.0.1",       "gateway_10_0_0_1"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("input_text", "desc"),
        PRIVATE_IP_REDACTED_CASES,
        ids=[c[1] for c in PRIVATE_IP_REDACTED_CASES],
    )
    def test_private_ip_redacted(self, input_text: str, desc: str) -> None:
        """Test non-gateway private IPs are redacted."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_string_patterns

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_string_patterns(input_text, collector=collector)
        assert input_text not in result, f"{desc}: private IP should be redacted"

    @pytest.mark.parametrize(
        ("input_text", "desc"),
        PRIVATE_IP_PRESERVED_CASES,
        ids=[c[1] for c in PRIVATE_IP_PRESERVED_CASES],
    )
    def test_gateway_ips_preserved(self, input_text: str, desc: str) -> None:
        """Test common gateway IPs are preserved (not redacted)."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_string_patterns

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_string_patterns(input_text, collector=collector)
        assert input_text in result, f"{desc}: gateway IP should be preserved"

    def test_public_ip_in_json_response(self) -> None:
        """Test public IP in JSON response body is sanitized end-to-end."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://192.168.1.1/api/status",
                "headers": [],
            },
            "response": {
                "status": 200,
                "headers": [],
                "content": {
                    "mimeType": "application/json",
                    "text": json.dumps({"wanIp": "73.158.42.197", "status": "connected"}),
                },
            },
        }
        result = sanitize_entry(entry, salt=None)
        content = json.loads(result["response"]["content"]["text"])
        assert "73.158.42.197" not in content["wanIp"], "Public IP should be redacted in JSON response"
        assert content["status"] == "connected", "Non-sensitive value should be preserved"

    def test_public_ip_collector_records(self) -> None:
        """Test that public IP redactions are recorded in collector."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_string_patterns

        hasher = Hasher.create(None)
        collector = RedactionCollector(hasher=hasher)
        _sanitize_string_patterns("WAN: 73.158.42.197", collector=collector)
        assert collector.auto_redacted_counts.get("public_ip", 0) > 0, "Should record public_ip redaction"


# =============================================================================
# Cookie Object Sanitization
# =============================================================================


class TestCookieObjectSanitization:
    """Tests for cookie object sanitization in request/response.

    Playwright parses Cookie and Set-Cookie headers into structured objects
    under request.cookies and response.cookies. These need sanitization
    alongside the header string sanitization.
    """

    def test_request_cookie_objects_sanitized(self) -> None:
        """Test request cookie objects have values redacted."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://192.168.1.1/",
                "headers": [],
                "cookies": [
                    {"name": "session_id", "value": "abc123secret"},
                    {"name": "tracking", "value": "xyz789token"},
                ],
            },
            "response": {
                "status": 200,
                "headers": [],
                "content": {"mimeType": "text/html", "text": "<html></html>"},
            },
        }
        result = sanitize_entry(entry, salt=None)
        for cookie in result["request"]["cookies"]:
            assert cookie["value"] != "abc123secret", f"Cookie {cookie['name']} value should be redacted"
            assert cookie["value"] != "xyz789token", f"Cookie {cookie['name']} value should be redacted"

    def test_response_cookie_objects_sanitized(self) -> None:
        """Test response cookie objects have values redacted."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://192.168.1.1/",
                "headers": [],
            },
            "response": {
                "status": 200,
                "headers": [],
                "cookies": [
                    {"name": "PHPSESSID", "value": "sess_abc123456"},
                    {"name": "auth_token", "value": "tok_secret789"},
                ],
                "content": {"mimeType": "text/html", "text": "<html></html>"},
            },
        }
        result = sanitize_entry(entry, salt=None)
        for cookie in result["response"]["cookies"]:
            assert cookie["value"] != "sess_abc123456", f"Cookie {cookie['name']} value should be redacted"
            assert cookie["value"] != "tok_secret789", f"Cookie {cookie['name']} value should be redacted"

    def test_cookie_names_preserved(self) -> None:
        """Test cookie names are not modified during sanitization."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://192.168.1.1/",
                "headers": [],
                "cookies": [
                    {"name": "session_id", "value": "secret123"},
                ],
            },
            "response": {
                "status": 200,
                "headers": [],
                "cookies": [
                    {"name": "PHPSESSID", "value": "secret456"},
                ],
                "content": {"mimeType": "text/html", "text": "<html></html>"},
            },
        }
        result = sanitize_entry(entry, salt=None)
        assert result["request"]["cookies"][0]["name"] == "session_id"
        assert result["response"]["cookies"][0]["name"] == "PHPSESSID"

    def test_empty_cookie_list_no_error(self) -> None:
        """Test empty cookie lists don't cause errors."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://192.168.1.1/",
                "headers": [],
                "cookies": [],
            },
            "response": {
                "status": 200,
                "headers": [],
                "cookies": [],
                "content": {"mimeType": "text/html", "text": "<html></html>"},
            },
        }
        sanitize_entry(entry, salt=None)  # Should not raise

    def test_cookie_hash_consistency(self) -> None:
        """Test same cookie value produces same hash across request and response."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://192.168.1.1/",
                "headers": [],
                "cookies": [
                    {"name": "token", "value": "shared_secret_value"},
                ],
            },
            "response": {
                "status": 200,
                "headers": [],
                "cookies": [
                    {"name": "token", "value": "shared_secret_value"},
                ],
                "content": {"mimeType": "text/html", "text": "<html></html>"},
            },
        }
        result = sanitize_entry(entry, salt="test-salt")
        req_hash = result["request"]["cookies"][0]["value"]
        resp_hash = result["response"]["cookies"][0]["value"]
        assert req_hash == resp_hash, "Same cookie value should produce same hash"


# =============================================================================
# Serial Number Field Detection in JSON
# =============================================================================


class TestSerialNumberJsonSanitization:
    """Tests for serial number field detection in JSON recursive sanitizer.

    JSON responses from device APIs often contain serial number fields
    that need redaction. These are detected by field name matching.
    """

    # fmt: off
    SERIAL_FIELD_CASES = [
        ("serial",        "SN827194729",   "lowercase_serial"),
        ("serial_number", "ABC123XYZ",     "snake_case"),
        ("serialNumber",  "DEF456",        "camelCase"),
        ("serialnum",     "GHI789",        "abbreviated"),
        ("sn",            "JKL012",        "two_letter"),
        ("Serial",        "MNO345",        "capitalized"),
        ("SERIAL_NUMBER", "PQR678",        "upper_snake"),
        ("SN",            "STU901",        "upper_two_letter"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("field_name", "serial_value", "desc"),
        SERIAL_FIELD_CASES,
        ids=[c[2] for c in SERIAL_FIELD_CASES],
    )
    def test_serial_field_redacted(self, field_name: str, serial_value: str, desc: str) -> None:
        """Test serial number fields are redacted in JSON."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://192.168.1.1/api/info",
                "headers": [],
            },
            "response": {
                "status": 200,
                "headers": [],
                "content": {
                    "mimeType": "application/json",
                    "text": json.dumps({field_name: serial_value, "model": "C7000"}),
                },
            },
        }
        result = sanitize_entry(entry, salt=None)
        content = json.loads(result["response"]["content"]["text"])
        assert content[field_name] != serial_value, f"{desc}: serial should be redacted"
        assert "SERIAL" in content[field_name], f"{desc}: should use SERIAL prefix"

    def test_serial_empty_value_preserved(self) -> None:
        """Test empty serial number values are not modified."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://192.168.1.1/api/info",
                "headers": [],
            },
            "response": {
                "status": 200,
                "headers": [],
                "content": {
                    "mimeType": "application/json",
                    "text": json.dumps({"serial": "", "model": "C7000"}),
                },
            },
        }
        result = sanitize_entry(entry, salt=None)
        content = json.loads(result["response"]["content"]["text"])
        assert content["serial"] == "", "Empty serial should be preserved"

    def test_serial_hash_consistency(self) -> None:
        """Test same serial value produces same hash."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector
        from har_capture.sanitization.har import _sanitize_json_recursive

        hasher = Hasher.create("test-salt")
        collector = RedactionCollector(hasher=hasher)
        data1 = {"serial": "SN827194729"}
        data2 = {"sn": "SN827194729"}
        result1 = _sanitize_json_recursive(data1, hasher, collector)
        result2 = _sanitize_json_recursive(data2, hasher, collector)
        assert result1["serial"] == result2["sn"], "Same serial value should produce same hash"

    def test_non_serial_field_not_affected(self) -> None:
        """Test non-serial fields are not treated as serial numbers."""
        entry = {
            "request": {
                "method": "GET",
                "url": "http://192.168.1.1/api/info",
                "headers": [],
            },
            "response": {
                "status": 200,
                "headers": [],
                "content": {
                    "mimeType": "application/json",
                    "text": json.dumps({"model": "C7000", "firmware": "V1.03.08"}),
                },
            },
        }
        result = sanitize_entry(entry, salt=None)
        content = json.loads(result["response"]["content"]["text"])
        assert content["model"] == "C7000", "Model should be preserved"
        assert content["firmware"] == "V1.03.08", "Firmware should be preserved"


class TestBrowserCookieSanitization:
    """Tests for browser_cookies sanitization in _har_capture metadata."""

    def test_browser_cookie_values_redacted(self) -> None:
        """Test that browser_cookies values in _har_capture metadata are redacted."""
        har = {
            "log": {
                "entries": [],
                "_har_capture": {
                    "browser_cookies": [
                        {
                            "name": "XSRF_TOKEN",
                            "value": "secret123",
                            "domain": ".example.com",
                            "path": "/",
                            "httpOnly": False,
                            "secure": True,
                            "sameSite": "Lax",
                        },
                        {
                            "name": "session_id",
                            "value": "s3cr3t-s3ss10n",
                            "domain": ".example.com",
                            "path": "/",
                            "httpOnly": True,
                            "secure": True,
                            "sameSite": "Strict",
                        },
                    ],
                },
            }
        }

        result, _ = sanitize_har(har, salt="test-salt")

        cookies = result["log"]["_har_capture"]["browser_cookies"]
        assert cookies[0]["value"] != "secret123", "Cookie value should be redacted"
        assert cookies[1]["value"] != "s3cr3t-s3ss10n", "Cookie value should be redacted"

    def test_browser_cookie_structural_properties_preserved(self) -> None:
        """Test that cookie structural properties (domain, path, httpOnly, secure) are preserved."""
        har = {
            "log": {
                "entries": [],
                "_har_capture": {
                    "browser_cookies": [
                        {
                            "name": "token",
                            "value": "abc",
                            "domain": ".example.com",
                            "path": "/api",
                            "httpOnly": True,
                            "secure": True,
                            "sameSite": "Strict",
                        },
                    ],
                },
            }
        }

        result, _ = sanitize_har(har, salt="test-salt")

        cookie = result["log"]["_har_capture"]["browser_cookies"][0]
        assert cookie["name"] == "token"
        assert cookie["domain"] == ".example.com"
        assert cookie["path"] == "/api"
        assert cookie["httpOnly"] is True
        assert cookie["secure"] is True
        assert cookie["sameSite"] == "Strict"

    def test_browser_cookies_absent_no_error(self) -> None:
        """Test sanitize_har works when _har_capture has no browser_cookies."""
        har = {
            "log": {
                "entries": [],
                "_har_capture": {"tool": "har-capture", "version": "0.4.1"},
            }
        }

        result, _ = sanitize_har(har, salt="test-salt")
        assert "browser_cookies" not in result["log"]["_har_capture"]

    def test_browser_cookies_empty_list(self) -> None:
        """Test sanitize_har handles empty browser_cookies list."""
        har = {
            "log": {
                "entries": [],
                "_har_capture": {"browser_cookies": []},
            }
        }

        result, _ = sanitize_har(har, salt="test-salt")
        assert result["log"]["_har_capture"]["browser_cookies"] == []


# ┌──────────────────────────────┬──────────────┬──────────────────────────────────────────────────────┬──────────────────────────────────────┐
# │ desc                         │ storage_key  │ har_capture_input                                    │ assertion                            │
# ├──────────────────────────────┼──────────────┼──────────────────────────────────────────────────────┼──────────────────────────────────────┤
# │ Test case name               │ Which key    │ _har_capture metadata                                │ Lambda checking result               │
# └──────────────────────────────┴──────────────┴──────────────────────────────────────────────────────┴──────────────────────────────────────┘
#
# fmt: off
WEB_STORAGE_SANITIZATION_CASES = [
    (
        "local_storage_values_redacted",
        "local_storage",
        {
            "local_storage": [
                {
                    "origin": "https://192.168.100.1",
                    "items": [
                        {"name": "PrivateKey", "value": "hmac_secret_123"},
                        {"name": "firmware_url", "value": "http://10.0.1.1/fw"},
                    ],
                },
            ],
        },
        lambda r: (
            r["local_storage"][0]["items"][0]["name"] == "PrivateKey"
            and r["local_storage"][0]["items"][0]["value"] != "hmac_secret_123"
            and r["local_storage"][0]["items"][1]["name"] == "firmware_url"
            and r["local_storage"][0]["items"][1]["value"] != "http://10.0.1.1/fw"
        ),
    ),
    (
        "session_storage_values_redacted",
        "session_storage",
        {
            "session_storage": [
                {
                    "origin": "https://192.168.100.1",
                    "items": [
                        {"name": "sjcl_key", "value": "aes256_key_abc"},
                        {"name": "csrf_token", "value": "xsrf_token_xyz"},
                    ],
                },
            ],
        },
        lambda r: (
            r["session_storage"][0]["items"][0]["name"] == "sjcl_key"
            and r["session_storage"][0]["items"][0]["value"] != "aes256_key_abc"
            and r["session_storage"][0]["items"][1]["name"] == "csrf_token"
            and r["session_storage"][0]["items"][1]["value"] != "xsrf_token_xyz"
        ),
    ),
    (
        "origin_preserved",
        "local_storage",
        {
            "local_storage": [
                {"origin": "https://192.168.100.1", "items": [{"name": "k", "value": "v"}]},
            ],
            "session_storage": [
                {"origin": "http://10.0.0.1:8080", "items": [{"name": "t", "value": "a"}]},
            ],
        },
        lambda r: (
            r["local_storage"][0]["origin"] == "https://192.168.100.1"
            and r["session_storage"][0]["origin"] == "http://10.0.0.1:8080"
        ),
    ),
    (
        "absent_no_error",
        "local_storage",
        {"tool": "har-capture", "version": "0.4.2"},
        lambda r: "local_storage" not in r and "session_storage" not in r,
    ),
    (
        "empty_lists_preserved",
        "local_storage",
        {"local_storage": [], "session_storage": []},
        lambda r: r["local_storage"] == [] and r["session_storage"] == [],
    ),
    (
        "storage_prefix_not_cookie",
        "local_storage",
        {
            "local_storage": [
                {"origin": "https://example.com", "items": [{"name": "key", "value": "secret_value"}]},
            ],
        },
        lambda r: r["local_storage"][0]["items"][0]["value"].startswith("STORAGE_"),
    ),
]
# fmt: on


class TestWebStorageSanitization:
    """Tests for web storage sanitization in _har_capture metadata."""

    @pytest.mark.parametrize(
        ("desc", "storage_key", "har_capture_input", "check"),
        WEB_STORAGE_SANITIZATION_CASES,
        ids=[c[0] for c in WEB_STORAGE_SANITIZATION_CASES],
    )
    def test_web_storage_sanitization(
        self,
        desc: str,
        storage_key: str,
        har_capture_input: dict,
        check: object,
    ) -> None:
        """Table-driven web storage sanitization tests."""
        har = {"log": {"entries": [], "_har_capture": har_capture_input}}

        result, _ = sanitize_har(har, salt="test-salt")

        meta = result["log"]["_har_capture"]
        assert check(meta), f"Assertion failed for case: {desc}"


class TestSanitizeJsonTextLogging:
    """Tests for _sanitize_json_text debug logging."""

    def test_sanitize_json_text_invalid_json_logs_debug(self, caplog: pytest.LogCaptureFixture) -> None:
        """Non-JSON text logs debug message."""
        import logging

        from har_capture.sanitization.har import _sanitize_json_text

        with caplog.at_level(logging.DEBUG):
            result = _sanitize_json_text("<html>not json</html>")
        assert result == "<html>not json</html>"
        assert "Non-JSON" in caplog.text


# =============================================================================
# Sanitization Metadata Embedding
# =============================================================================

# ┌──────────────────────┬────────────┬──────────────────────────────────────┐
# │ salt_arg             │ expected   │ description                          │
# ├──────────────────────┼────────────┼──────────────────────────────────────┤
SALT_MODE_CASES = [
    ("auto", "random", "auto keyword → random mode"),
    ("random", "random", "random keyword → random mode"),
    (None, "static", "None → static mode"),
    ("my-salt", "provided", "user string → provided mode"),
    ("s3cret!", "provided", "special chars → provided mode"),
]
# └──────────────────────┴────────────┴──────────────────────────────────────┘

# ┌──────────────────────┬──────────────────────────────────────────────────┐
# │ heuristic_mode       │ expected_value                                  │
# ├──────────────────────┼──────────────────────────────────────────────────┤
HEURISTICS_MODE_CASES = [
    (HeuristicMode.DISABLED, "disabled"),
    (HeuristicMode.FLAG, "flag"),
    (HeuristicMode.REDACT, "redact"),
]
# └──────────────────────┴──────────────────────────────────────────────────┘

# Required keys in the sanitization metadata block
_REQUIRED_METADATA_KEYS = frozenset(
    {
        "tool",
        "version",
        "sanitized_at",
        "salt_mode",
        "heuristics",
        "auto_redacted",
        "auto_redacted_counts",
        "user_redacted",
        "user_skipped",
        "flagged_total",
        "warnings",
    }
)


def _make_report(**overrides: object) -> SanitizationReport:
    """Build a minimal SanitizationReport with optional overrides."""
    defaults: dict[str, object] = {
        "input_file": "",
        "output_file": "",
        "salt": "test-salt",
        "auto_redacted_counts": {"password": 3, "mac_address": 2},
    }
    defaults.update(overrides)
    return SanitizationReport(**defaults)  # type: ignore[arg-type]


class TestEmbedSanitizationMetadataUnit:
    """Unit tests for _embed_sanitization_metadata() called directly."""

    def test_all_required_keys_present(self) -> None:
        """Metadata block contains every required key."""
        har: dict[str, object] = {"log": {}}
        _embed_sanitization_metadata(har, _make_report(), HeuristicMode.DISABLED, "auto")
        meta = har["log"]["_har_capture"]["sanitization"]  # type: ignore[index]
        assert set(meta.keys()) == _REQUIRED_METADATA_KEYS

    @pytest.mark.parametrize(
        ("salt_arg", "expected_mode", "desc"),
        SALT_MODE_CASES,
        ids=[c[2] for c in SALT_MODE_CASES],
    )
    def test_salt_mode_mapping(
        self,
        salt_arg: str | None,
        expected_mode: str,
        desc: str,
    ) -> None:
        """Salt argument maps to correct salt_mode string."""
        har: dict[str, object] = {"log": {}}
        _embed_sanitization_metadata(har, _make_report(), HeuristicMode.DISABLED, salt_arg)
        assert har["log"]["_har_capture"]["sanitization"]["salt_mode"] == expected_mode  # type: ignore[index]

    @pytest.mark.parametrize(
        ("mode", "expected_value"),
        HEURISTICS_MODE_CASES,
        ids=[m.value for m, _ in HEURISTICS_MODE_CASES],
    )
    def test_heuristics_mode_recorded(
        self,
        mode: HeuristicMode,
        expected_value: str,
    ) -> None:
        """Each HeuristicMode enum is serialised to its .value string."""
        har: dict[str, object] = {"log": {}}
        _embed_sanitization_metadata(har, _make_report(), mode, "auto")
        assert har["log"]["_har_capture"]["sanitization"]["heuristics"] == expected_value  # type: ignore[index]

    def test_report_counts_propagated(self) -> None:
        """Auto-redacted counts from the report appear in metadata."""
        report = _make_report(auto_redacted_counts={"password": 5, "mac_address": 12})
        har: dict[str, object] = {"log": {}}
        _embed_sanitization_metadata(har, report, HeuristicMode.DISABLED, "auto")
        meta = har["log"]["_har_capture"]["sanitization"]  # type: ignore[index]
        assert meta["auto_redacted"] == 17
        assert meta["auto_redacted_counts"] == {"password": 5, "mac_address": 12}

    def test_does_not_leak_salt_value(self) -> None:
        """Actual salt string must never appear anywhere in the metadata."""
        report = _make_report(salt="super-secret-salt")
        har: dict[str, object] = {"log": {}}
        _embed_sanitization_metadata(har, report, HeuristicMode.DISABLED, "super-secret-salt")
        meta_json = json.dumps(har["log"]["_har_capture"]["sanitization"])  # type: ignore[index]
        assert "super-secret-salt" not in meta_json
        # Only salt-related key should be salt_mode
        assert set(k for k in har["log"]["_har_capture"]["sanitization"] if "salt" in k) == {"salt_mode"}  # type: ignore[index]

    def test_preserves_existing_har_capture_keys(self) -> None:
        """Existing _har_capture fields are not clobbered."""
        har: dict[str, object] = {
            "log": {"_har_capture": {"version": "0.4.3", "browser_cookies": []}},
        }
        _embed_sanitization_metadata(har, _make_report(), HeuristicMode.DISABLED, "auto")
        cap = har["log"]["_har_capture"]  # type: ignore[index]
        assert cap["version"] == "0.4.3"
        assert cap["browser_cookies"] == []
        assert "sanitization" in cap

    def test_creates_log_and_har_capture_if_missing(self) -> None:
        """Works even when log and _har_capture don't exist yet."""
        har: dict[str, object] = {}
        _embed_sanitization_metadata(har, _make_report(), HeuristicMode.DISABLED, "auto")
        assert "sanitization" in har["log"]["_har_capture"]  # type: ignore[index]

    def test_user_redacted_and_skipped_defaults(self) -> None:
        """Non-interactive report yields zero user counts."""
        har: dict[str, object] = {"log": {}}
        _embed_sanitization_metadata(har, _make_report(), HeuristicMode.DISABLED, "auto")
        meta = har["log"]["_har_capture"]["sanitization"]  # type: ignore[index]
        assert meta["user_redacted"] == 0
        assert meta["user_skipped"] == 0
        assert meta["flagged_total"] == 0
        assert meta["warnings"] == []

    def test_sanitized_at_is_utc_iso(self) -> None:
        """Timestamp is a valid UTC ISO-8601 string."""
        from datetime import datetime, timezone

        har: dict[str, object] = {"log": {}}
        _embed_sanitization_metadata(har, _make_report(), HeuristicMode.DISABLED, "auto")
        ts = har["log"]["_har_capture"]["sanitization"]["sanitized_at"]  # type: ignore[index]
        parsed = datetime.fromisoformat(ts)
        assert parsed.tzinfo is not None
        assert parsed.utcoffset() == timezone.utc.utcoffset(None)


class TestSanitizationMetadataIntegration:
    """Integration tests: sanitize_har() embeds metadata end-to-end."""

    def test_sanitize_har_embeds_metadata(self) -> None:
        """sanitize_har() output contains sanitization metadata with all keys."""
        result, _ = sanitize_har({"log": {"entries": []}})
        meta = result["log"]["_har_capture"]["sanitization"]
        assert set(meta.keys()) == _REQUIRED_METADATA_KEYS
        assert meta["tool"] == "har-capture"

    def test_records_redaction_counts(self) -> None:
        """HAR with sensitive data produces non-zero auto_redacted counts."""
        har = {
            "log": {
                "entries": [
                    {
                        "request": {
                            "url": "http://example.com",
                            "headers": [{"name": "Authorization", "value": "Bearer tok123"}],
                        },
                        "response": {
                            "headers": [],
                            "content": {"text": "", "mimeType": "text/html"},
                        },
                    }
                ]
            }
        }
        result, _ = sanitize_har(har, salt="test")
        meta = result["log"]["_har_capture"]["sanitization"]
        assert meta["auto_redacted"] > 0
        assert isinstance(meta["auto_redacted_counts"], dict)
        assert len(meta["auto_redacted_counts"]) > 0

    def test_preserves_existing_metadata(self) -> None:
        """Existing _har_capture fields are not clobbered by sanitize_har()."""
        har = {
            "log": {
                "entries": [],
                "_har_capture": {
                    "version": "0.4.3",
                    "browser_cookies": [{"name": "sid", "value": "abc"}],
                },
            }
        }
        result, _ = sanitize_har(har, salt="test")
        cap = result["log"]["_har_capture"]
        assert "sanitization" in cap
        assert "browser_cookies" in cap
        assert cap["version"] == "0.4.3"


# =============================================================================
# Base64 Credential Detection in URL Query Parameters
# =============================================================================

# ┌──────────────────────────────────────────────────────────────┬──────────┬──────────────────────────┐
# │ url                                                          │ redacted │ description              │
# ├──────────────────────────────────────────────────────────────┼──────────┼──────────────────────────┤
# │ Full URL with query string                                   │ True/    │ test case name           │
# │                                                              │ False    │                          │
# └──────────────────────────────────────────────────────────────┴──────────┴──────────────────────────┘
#
# fmt: off
BASE64_CRED_URL_CASES = [
    # Bare base64 token as query param (URL token auth pattern)
    ("https://192.168.100.1/status.html?YWRtaW46cGFzc3dvcmQ=", True,  "bare_base64_user_pass"),
    ("https://192.168.100.1/status.html?YWRtaW46TjZyM2gydCFy", True,  "bare_base64_no_padding"),
    # Base64 token as param value
    ("https://modem.local/api?token=YWRtaW46cGFzc3dvcmQ=",     True,  "base64_as_param_value"),
    # Base64 cred in value with non-sensitive key (exercises URL-level detection)
    ("https://modem.local/api?ref=YWRtaW46cGFzc3dvcmQ=",      True,  "base64_val_nonsensitive_key"),
    # Normal query params (should NOT be detected)
    ("https://example.com/page?id=123&format=json",             False, "normal_params"),
    ("https://example.com/page?q=hello+world",                  False, "normal_search_query"),
    # Non-credential base64 (no colon in decoded)
    ("https://example.com/page?data=aGVsbG8gd29ybGQ=",         False, "base64_without_colon"),
    # Short values (too short to be credentials)
    ("https://example.com/page?x=YQ==",                        False, "too_short_base64"),
]
# fmt: on


class TestBase64CredentialDetection:
    """Tests for base64-encoded credential detection in URL query parameters."""

    @pytest.mark.parametrize(
        ("url", "should_redact", "desc"),
        BASE64_CRED_URL_CASES,
        ids=[c[2] for c in BASE64_CRED_URL_CASES],
    )
    def test_base64_credential_in_url(self, url: str, should_redact: bool, desc: str) -> None:
        """Test base64-encoded user:pass credentials are detected in URLs."""
        entry = {
            "request": {"method": "GET", "url": url, "headers": [], "queryString": []},
            "response": {"status": 200, "headers": [], "content": {}},
        }
        result = sanitize_entry(entry, salt="test")
        result_url = result["request"]["url"]
        if should_redact:
            assert result_url != url, f"{desc}: URL should be modified"
        else:
            assert result_url == url, f"{desc}: URL should be unchanged"

    def test_base64_credential_in_query_string_array(self) -> None:
        """Test base64 credentials in structured queryString array."""
        entry = {
            "request": {
                "method": "GET",
                "url": "https://192.168.100.1/status.html?YWRtaW46cGFzc3dvcmQ=",
                "headers": [],
                "queryString": [{"name": "YWRtaW46cGFzc3dvcmQ=", "value": ""}],
            },
            "response": {"status": 200, "headers": [], "content": {}},
        }
        result = sanitize_entry(entry, salt="test")
        qs = result["request"]["queryString"]
        assert qs[0]["name"] != "YWRtaW46cGFzc3dvcmQ=", "Base64 cred should be redacted in queryString"

    def test_base64_credential_in_query_string_value(self) -> None:
        """Test base64 credential in queryString param value is redacted."""
        entry = {
            "request": {
                "method": "GET",
                "url": "https://example.com/api",
                "headers": [],
                "queryString": [{"name": "token", "value": "YWRtaW46cGFzc3dvcmQ="}],
            },
            "response": {"status": 200, "headers": [], "content": {}},
        }
        result = sanitize_entry(entry, salt="test")
        qs = result["request"]["queryString"]
        assert qs[0]["value"] != "YWRtaW46cGFzc3dvcmQ=", "Base64 cred in value should be redacted"

    def test_base64_credential_bare_name_without_padding(self) -> None:
        """Test base64 credential as bare param name with padding stripped."""
        import base64

        cred = base64.b64encode(b"user:secret").decode()  # dXNlcjpzZWNyZXQ=
        stripped = cred.rstrip("=")
        entry = {
            "request": {
                "method": "GET",
                "url": "https://example.com/api",
                "headers": [],
                "queryString": [{"name": stripped, "value": ""}],
            },
            "response": {"status": 200, "headers": [], "content": {}},
        }
        result = sanitize_entry(entry, salt="test")
        qs = result["request"]["queryString"]
        assert qs[0]["name"] != stripped, "Base64 cred name (padding stripped) should be redacted"


class TestUrlQueryBareSegment:
    """Tests for URL query segments without '=' that are not base64 credentials."""

    def test_bare_query_segment_preserved(self) -> None:
        """Test bare query segment (no '=', not base64 cred) passes through unchanged."""
        entry = {
            "request": {
                "method": "GET",
                "url": "https://example.com/page?debug&password=secret",
                "headers": [],
                "queryString": [],
            },
            "response": {"status": 200, "headers": [], "content": {}},
        }
        result = sanitize_entry(entry, salt="test")
        result_url = result["request"]["url"]
        assert "debug" in result_url, "Bare non-credential segment should be preserved"
        assert "secret" not in result_url, "Sensitive param value should be redacted"


class TestCookieHeaderNoNameValuePairs:
    """Tests for cookie headers with no name=value pairs."""

    def test_cookie_header_bare_value_redacted(self) -> None:
        """Test cookie header with bare value (no = sign) is fully redacted."""
        result = sanitize_header_value("Cookie", "opaque_session_token_abc123")
        assert result != "opaque_session_token_abc123", "Bare cookie value should be redacted"

    def test_set_cookie_header_bare_value_redacted(self) -> None:
        """Test Set-Cookie header with bare value (no = sign) is fully redacted."""
        result = sanitize_header_value("Set-Cookie", "some_random_token")
        assert result != "some_random_token", "Bare Set-Cookie value should be redacted"


# =============================================================================
# Cookie Attribute Metadata Sanitization
# =============================================================================

# ┌─────────────┬──────────────────────────────────────┬──────────┬────────────────────────────────┐
# │ header_name │ header_value                         │ redacted │ description                    │
# ├─────────────┼──────────────────────────────────────┼──────────┼────────────────────────────────┤
# │ Header name │ Header value to sanitize             │ True/    │ test case name                 │
# │             │                                      │ False    │                                │
# └─────────────┴──────────────────────────────────────┴──────────┴────────────────────────────────┘
#
# fmt: off
COOKIE_ATTR_CASES = [
    # Attribute metadata (should be redacted as non-cookie data)
    ("Set-Cookie", "HttpOnly: true, Secure: true",      True,  "attribute_metadata_set_cookie"),
    ("Cookie",     "HttpOnly: true, Secure: true",      True,  "attribute_metadata_cookie"),
    ("Set-Cookie", "Secure",                            True,  "bare_secure_attribute"),
    # Normal cookies (should be redacted as cookie values)
    ("Set-Cookie", "session_id=abc123; HttpOnly; Secure", True,  "normal_set_cookie_with_attrs"),
    ("Cookie",     "session=abc123",                    True,  "normal_cookie"),
    # Non-cookie header (should be unchanged)
    ("Content-Type", "text/html",                       False, "non_cookie_header"),
]
# fmt: on


class TestCookieAttributeMetadata:
    """Tests for cookie attribute metadata sanitization."""

    @pytest.mark.parametrize(
        ("header_name", "header_value", "should_redact", "desc"),
        COOKIE_ATTR_CASES,
        ids=[c[3] for c in COOKIE_ATTR_CASES],
    )
    def test_cookie_attribute_metadata(
        self,
        header_name: str,
        header_value: str,
        should_redact: bool,
        desc: str,
    ) -> None:
        """Test cookie headers with attribute metadata are properly handled."""
        result = sanitize_header_value(header_name, header_value)
        if should_redact:
            assert result != header_value, f"{desc}: should be redacted"
        else:
            assert result == header_value, f"{desc}: should be unchanged"


# =============================================================================
# Pre-existing Coverage Gaps
# =============================================================================


class TestValidateHarStructure:
    """Tests for validate_har_structure error and warning paths."""

    # fmt: off
    VALIDATION_ERROR_CASES = [
        ({"log": "not_a_dict"},                                  "'log' must be an object",   "log_not_dict"),
        ({"log": {"version": "1.2"}},                            "Missing required 'entries'", "missing_entries"),
        ({"log": {"entries": "not_a_list"}},                     "'entries' must be an array", "entries_not_list"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("har_data", "expected_msg", "desc"),
        VALIDATION_ERROR_CASES,
        ids=[c[2] for c in VALIDATION_ERROR_CASES],
    )
    def test_structure_errors(self, har_data: dict, expected_msg: str, desc: str) -> None:
        """Test validate_har_structure raises on invalid structures."""
        with pytest.raises(HarValidationError, match=expected_msg):
            validate_har_structure(har_data)

    def test_missing_recommended_fields_warns(self) -> None:
        """Test warnings for missing log.version and log.creator."""
        har_data = {"log": {"entries": []}}
        warnings = validate_har_structure(har_data)
        assert any("version" in w for w in warnings)
        assert any("creator" in w for w in warnings)

    # fmt: off
    STRICT_CASES = [
        ({"log": {"entries": ["not_a_dict"]}},                                        "not an object",    "entry_not_dict"),
        ({"log": {"entries": [{}]}},                                                  "missing 'request'", "entry_missing_request"),
        ({"log": {"entries": [{"request": {"url": "/a"}}]}},                          "missing 'method'", "request_missing_method"),
        ({"log": {"entries": [{"request": {"method": "GET"}}]}},                      "missing 'url'",    "request_missing_url"),
        ({"log": {"entries": [{"request": {"method": "GET", "url": "/a"}}]}},         "missing 'response'", "entry_missing_response"),
        ({"log": {"entries": [{"request": {"method": "GET", "url": "/a"}, "response": {}}]}}, "missing 'status'", "response_missing_status"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("har_data", "expected_warning", "desc"),
        STRICT_CASES,
        ids=[c[2] for c in STRICT_CASES],
    )
    def test_strict_mode_warnings(self, har_data: dict, expected_warning: str, desc: str) -> None:
        """Test strict mode catches missing fields."""
        warnings = validate_har_structure(har_data, strict=True)
        assert any(expected_warning in w for w in warnings), (
            f"{desc}: expected warning containing '{expected_warning}'"
        )


class TestFormUrlencodedSanitization:
    """Tests for _sanitize_form_urlencoded edge cases."""

    def test_flaggable_field_recorded(self) -> None:
        """Test flaggable field in form data is flagged via collector."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector

        hasher = Hasher.create("test")
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_form_urlencoded("username=johndoe&page=1", hasher, collector)
        assert "johndoe" in result, "Flaggable field value should be preserved (not auto-redacted)"
        assert len(collector.flagged) > 0, "Should have flagged the username field"

    def test_bare_segment_without_equals(self) -> None:
        """Test bare segment (no '=') is passed through."""
        result = _sanitize_form_urlencoded("bare_value&password=secret", None, None)
        assert "bare_value" in result
        assert "secret" not in result


class TestJsonRecursiveSanitization:
    """Tests for _sanitize_json_recursive edge cases."""

    # fmt: off
    JSON_EDGE_CASES = [
        # MAC address field — with and without hasher
        ({"mac": "AA:BB:CC:DD:EE:FF"},       "mac",    "AA:BB:CC:DD:EE:FF", "mac_field_redacted"),
        ({"macaddress": "11:22:33:44:55:66"}, "macaddress", "11:22:33:44:55:66", "macaddress_field_redacted"),
        ({"mac": ""},                         "mac",    "",                  "mac_empty_preserved"),
        ({"mac": 42},                         "mac",    42,                  "mac_non_string_preserved"),
        # Serial number field
        ({"serial": "ABC12345678"},           "serial", "ABC12345678",       "serial_field_redacted"),
        ({"sn": "XYZ987"},                    "sn",     "XYZ987",            "sn_field_redacted"),
        ({"serial": ""},                      "serial", "",                  "serial_empty_preserved"),
        ({"serial": 99},                      "serial", 99,                  "serial_non_string_preserved"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("data", "field", "original_value", "desc"),
        JSON_EDGE_CASES,
        ids=[c[3] for c in JSON_EDGE_CASES],
    )
    def test_json_recursive_edge_cases(
        self, data: dict, field: str, original_value: object, desc: str
    ) -> None:
        """Test MAC and serial field handling in JSON recursive sanitization."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector

        hasher = Hasher.create("test")
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_json_recursive(data, hasher, collector)

        if isinstance(original_value, str) and original_value:
            assert result[field] != original_value, f"{desc}: should be redacted"
        else:
            assert result[field] == original_value, f"{desc}: should be preserved"

    def test_mac_field_without_hasher(self) -> None:
        """Test MAC field uses placeholder when no hasher provided."""
        result = _sanitize_json_recursive({"mac": "AA:BB:CC:DD:EE:FF"}, None, None)
        assert result["mac"] == "***MAC***"

    def test_serial_field_without_hasher(self) -> None:
        """Test serial field uses placeholder when no hasher provided."""
        result = _sanitize_json_recursive({"serial": "ABC123"}, None, None)
        assert result["serial"] == "***SERIAL***"

    def test_bare_primitive_passthrough(self) -> None:
        """Test non-dict/list/str values pass through unchanged."""
        assert _sanitize_json_recursive(42, None, None) == 42
        assert _sanitize_json_recursive(True, None, None) is True
        assert _sanitize_json_recursive(None, None, None) is None


class TestStringPatternSanitization:
    """Tests for _sanitize_string_patterns edge cases."""

    def test_empty_string_returns_empty(self) -> None:
        """Test empty string is returned unchanged."""
        assert _sanitize_string_patterns("") == ""

    def test_mac_without_hasher(self) -> None:
        """Test MAC replacement uses placeholder when no hasher."""
        result = _sanitize_string_patterns("Device MAC: AA:BB:CC:DD:EE:FF")
        assert "AA:BB:CC:DD:EE:FF" not in result
        assert "***MAC***" in result

    def test_mac_without_collector(self) -> None:
        """Test MAC replacement works without a collector."""
        from har_capture.patterns import Hasher

        hasher = Hasher.create("test")
        result = _sanitize_string_patterns("Device MAC: AA:BB:CC:DD:EE:FF", hasher, None)
        assert "AA:BB:CC:DD:EE:FF" not in result

    def test_public_ip_without_hasher(self) -> None:
        """Test public IP replacement uses placeholder when no hasher."""
        result = _sanitize_string_patterns("DNS: 8.8.8.8")
        assert "8.8.8.8" not in result
        assert "***IP***" in result

    def test_public_ip_without_collector(self) -> None:
        """Test public IP replacement works without a collector."""
        from har_capture.patterns import Hasher

        hasher = Hasher.create("test")
        result = _sanitize_string_patterns("DNS: 8.8.8.8", hasher, None)
        assert "8.8.8.8" not in result

    def test_email_without_hasher(self) -> None:
        """Test email replacement uses placeholder when no hasher."""
        result = _sanitize_string_patterns("Contact: admin@example.com")
        assert "admin@example.com" not in result
        assert "***EMAIL***" in result

    def test_email_without_collector(self) -> None:
        """Test email replacement works without a collector."""
        from har_capture.patterns import Hasher

        hasher = Hasher.create("test")
        result = _sanitize_string_patterns("Contact: admin@example.com", hasher, None)
        assert "admin@example.com" not in result

    def test_credit_card_luhn_valid(self) -> None:
        """Test valid credit card number is redacted."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector

        hasher = Hasher.create("test")
        collector = RedactionCollector(hasher=hasher)
        # Visa test number that passes Luhn
        result = _sanitize_string_patterns("Card: 4111111111111111", hasher, collector)
        assert "4111111111111111" not in result

    def test_credit_card_luhn_invalid(self) -> None:
        """Test invalid credit card number (fails Luhn) is preserved."""
        # Visa-format but fails Luhn check
        result = _sanitize_string_patterns("Number: 4111111111111112", None, None)
        assert "4111111111111112" in result

    def test_string_patterns_with_collector_records_mac(self) -> None:
        """Test MAC in string patterns records redaction via collector."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector

        hasher = Hasher.create("test")
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_string_patterns("Device MAC: AA:BB:CC:DD:EE:FF", hasher, collector)
        assert "AA:BB:CC:DD:EE:FF" not in result
        assert collector.auto_redacted_counts.get("mac_address", 0) > 0

    def test_string_patterns_with_collector_records_public_ip(self) -> None:
        """Test public IP in string patterns records redaction via collector."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector

        hasher = Hasher.create("test")
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_string_patterns("DNS: 8.8.8.8", hasher, collector)
        assert "8.8.8.8" not in result
        assert collector.auto_redacted_counts.get("public_ip", 0) > 0

    def test_string_patterns_version_string_not_treated_as_public_ip(self) -> None:
        """Test version-like string matching public IP regex is preserved."""
        result = _sanitize_string_patterns("Version: 5.7.1.5", None, None)
        assert "5.7.1.5" in result, "Version string should not be treated as a public IP"

    def test_string_patterns_with_collector_records_email(self) -> None:
        """Test email in string patterns records redaction via collector."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector

        hasher = Hasher.create("test")
        collector = RedactionCollector(hasher=hasher)
        result = _sanitize_string_patterns("Contact: admin@example.com", hasher, collector)
        assert "admin@example.com" not in result
        assert collector.auto_redacted_counts.get("email", 0) > 0


class TestPostDataSanitizationEdgeCases:
    """Tests for sanitize_post_data edge cases."""

    def test_form_urlencoded_text_sanitized(self) -> None:
        """Test form-urlencoded mimeType triggers text sanitization."""
        post_data = {
            "mimeType": "application/x-www-form-urlencoded",
            "text": "password=secret123&action=login",
        }
        result = sanitize_post_data(post_data)
        assert "secret123" not in result["text"]
        assert "action=login" in result["text"]

    def test_json_text_sanitized(self) -> None:
        """Test application/json mimeType triggers JSON sanitization."""
        post_data = {
            "mimeType": "application/json",
            "text": '{"password": "secret", "user": "admin"}',
        }
        result = sanitize_post_data(post_data)
        parsed = json.loads(result["text"])
        assert parsed["password"] != "secret"

    def test_flaggable_param_in_params_array(self) -> None:
        """Test flaggable field in POST params array is flagged."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector

        hasher = Hasher.create("test")
        collector = RedactionCollector(hasher=hasher)
        post_data = {
            "params": [{"name": "username", "value": "johndoe"}],
        }
        result = sanitize_post_data(post_data, hasher, collector)
        assert result["params"][0]["value"] == "johndoe", "Flaggable value should be preserved"
        assert len(collector.flagged) > 0, "Should have flagged the username"


class TestUrlPathFlagging:
    """Tests for _sanitize_url_path flagging with collector."""

    # fmt: off
    PATH_FLAG_CASES = [
        ("http://api.example.com/users/550e8400-e29b-41d4-a716-446655440000/profile", "uuid",          "uuid_flagged"),
        ("http://api.example.com/keys/sk-1234567890abcdefghij/verify",                "api_key",       "api_key_flagged"),
        ("http://api.example.com/devices/DEV-ABC123456/status",                       "device_serial", "device_serial_flagged"),
        ("http://api.example.com/tokens/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6/refresh",   "token",         "long_token_flagged"),
    ]
    # fmt: on

    @pytest.mark.parametrize(
        ("url", "expected_category", "desc"),
        PATH_FLAG_CASES,
        ids=[c[2] for c in PATH_FLAG_CASES],
    )
    def test_url_path_segments_flagged(self, url: str, expected_category: str, desc: str) -> None:
        """Test suspicious path segments are flagged via collector."""
        from har_capture.patterns import Hasher
        from har_capture.sanitization.collector import RedactionCollector

        hasher = Hasher.create("test")
        collector = RedactionCollector(hasher=hasher)
        entry = {
            "request": {"method": "GET", "url": url, "headers": [], "queryString": []},
            "response": {"status": 200, "headers": [], "content": {}},
        }
        sanitize_entry(entry, salt="test", collector=collector)
        categories = {f.category for f in collector.flagged}
        assert expected_category in categories, f"{desc}: expected {expected_category} in {categories}"


class TestWebStorageNonDictOrigin:
    """Tests for web storage with non-dict origin entries."""

    def test_non_dict_origin_entry_skipped(self) -> None:
        """Test non-dict entries in web storage list are skipped."""
        har = {
            "log": {
                "entries": [],
                "_har_capture": {
                    "local_storage": [
                        "not_a_dict",
                        {"origin": "https://example.com", "items": [{"name": "k", "value": "secret"}]},
                    ],
                },
            }
        }
        result, _ = sanitize_har(har, salt="test")
        storage = result["log"]["_har_capture"]["local_storage"]
        assert storage[0] == "not_a_dict", "Non-dict entry should be passed through"
        assert storage[1]["items"][0]["value"] != "secret", "Valid entry should be sanitized"


class TestSanitizeHarFileEdgeCases:
    """Tests for sanitize_har_file edge cases."""

    def test_non_har_extension_output_path(self, tmp_path) -> None:
        """Test input file without .har extension gets .sanitized.har appended."""
        har_data = {"log": {"entries": []}}
        input_file = tmp_path / "capture.json"
        input_file.write_text(json.dumps(har_data))

        output_path, _ = sanitize_har_file(input_file)
        assert output_path.endswith(".sanitized.har")
        assert "capture.json.sanitized.har" in output_path


class TestApplyUserRedactions:
    """Tests for apply_user_redactions error paths."""

    def test_redaction_error_continues(self) -> None:
        """Test that a failing redaction item doesn't stop other redactions."""
        from unittest.mock import patch

        har_data = {"log": {"entries": [{"request": {"url": "http://test/"}, "response": {}}]}}
        report = SanitizationReport(
            input_file="",
            output_file="",
            salt="test",
            flagged=[
                FlaggedValue(
                    original_value="first_value",
                    category="test",
                    confidence="HIGH",
                    context="ctx",
                    reason="test",
                    status=RedactionStatus.USER_REDACTED,
                ),
                FlaggedValue(
                    original_value="http://test/",
                    category="url",
                    confidence="HIGH",
                    context="ctx",
                    reason="test url",
                    status=RedactionStatus.USER_REDACTED,
                ),
            ],
        )
        # Mock hasher to fail on first call, succeed on second
        with patch("har_capture.sanitization.har.Hasher") as mock_hasher_cls:
            mock_hash = mock_hasher_cls.create.return_value.hash_generic
            mock_hash.side_effect = [RuntimeError("hash failed"), "REDACTED_URL"]
            result = apply_user_redactions(har_data, report)
        assert isinstance(result, dict), "Should return valid result despite first item failing"
        # The second redaction should still have been attempted
        assert mock_hash.call_count == 2, "Both redaction items should have been attempted"

    def test_invalid_har_data_raises(self) -> None:
        """Test apply_user_redactions raises on invalid input."""
        report = SanitizationReport(input_file="", output_file="", salt="test")
        with pytest.raises(HarValidationError):
            apply_user_redactions("not_a_dict", report)  # type: ignore[arg-type]

    def test_missing_log_raises(self) -> None:
        """Test apply_user_redactions raises when 'log' key missing."""
        report = SanitizationReport(input_file="", output_file="", salt="test")
        with pytest.raises(HarValidationError, match="log"):
            apply_user_redactions({}, report)

    def test_redaction_item_exception_is_caught(self) -> None:
        """Test that an exception during a single redaction is caught and logged."""
        from unittest.mock import patch

        har_data = {"log": {"entries": []}}
        report = SanitizationReport(
            input_file="",
            output_file="",
            salt="test",
            flagged=[
                FlaggedValue(
                    original_value="test_value",
                    category="test",
                    confidence="HIGH",
                    context="ctx",
                    reason="test",
                    status=RedactionStatus.USER_REDACTED,
                ),
            ],
        )
        with patch("har_capture.sanitization.har.Hasher") as mock_hasher_cls:
            mock_hasher_cls.create.return_value.hash_generic.side_effect = RuntimeError("hash failed")
            result = apply_user_redactions(har_data, report)
        assert isinstance(result, dict), "Should return valid result despite error"

    def test_json_decode_error_after_replacement(self) -> None:
        """Test HarValidationError raised if json.loads fails after replacement."""
        from unittest.mock import patch

        har_data = {"log": {"entries": [{"note": "target_value"}]}}
        report = SanitizationReport(
            input_file="",
            output_file="",
            salt="test",
            flagged=[
                FlaggedValue(
                    original_value="target_value",
                    category="test",
                    confidence="HIGH",
                    context="ctx",
                    reason="test",
                    status=RedactionStatus.USER_REDACTED,
                ),
            ],
        )
        with (
            patch(
                "har_capture.sanitization.har.json.loads",
                side_effect=json.JSONDecodeError("broken", "", 0),
            ),
            pytest.raises(HarValidationError, match="Failed to parse HAR"),
        ):
            apply_user_redactions(har_data, report)
