"""Redaction — the gate ADR-0002 requires before anything reaches a model.

The ADR names this suite explicitly: real-shaped fixtures for CNIC, IBAN, phone
and email, including boundary cases. Masking is the one control standing between
a client's identity documents and a third party's retention window.
"""

from __future__ import annotations

import pytest

from app.security.redaction import (
    DataLevel,
    RestrictedDataError,
    assert_no_restricted,
    mask_address,
    mask_cnic,
    mask_email,
    mask_iban,
    mask_mobile,
    mask_name,
    redact_text,
)


class TestMasking:
    def test_a_cnic_keeps_only_its_province_prefix(self):
        # The prefix is not identifying alone and lets a model reason about
        # region; everything that names a person does not survive.
        assert mask_cnic("35202-1234567-1") == "35202-XXXXXXX-X"

    def test_an_undashed_cnic_is_still_masked(self):
        assert mask_cnic("3520212345671") == "35202-XXXXXXX-X"

    def test_a_name_becomes_initials(self):
        # Settled in ADR-0002: a blank stops a model telling two people apart
        # within one workflow; a full name defeats the level.
        assert mask_name("Muhammad Ali") == "M. A."

    def test_a_single_name_still_masks(self):
        assert mask_name("Ali") == "A."

    def test_a_mobile_keeps_only_its_country_code(self):
        assert mask_mobile("923001234567") == "92**********"

    def test_an_email_keeps_its_domain(self):
        # Domains are rarely identifying and often meaningful — a firm, a bank.
        assert mask_email("ali@example.com") == "a***@example.com"

    def test_an_iban_keeps_enough_to_recognise_not_enough_to_pay(self):
        masked = mask_iban("PK36SCBL0000001123456702")

        assert masked.startswith("PK36")
        assert masked.endswith("6702")
        assert "SCBL000000112345" not in masked

    def test_an_address_keeps_only_its_city(self):
        assert mask_address("House 12, Street 4, Gulshan, Karachi") == "Karachi"


class TestBoundaries:
    """The cases that break naive masking."""

    @pytest.mark.parametrize("value", ["", "   "])
    def test_empty_input_does_not_crash(self, value):
        assert mask_name(value) == value

    def test_a_short_iban_is_fully_masked(self):
        # Too short to reveal a prefix and a suffix without revealing all of it.
        assert set(mask_iban("PK36SCBL")) == {"*"}

    def test_a_short_number_is_fully_masked(self):
        assert set(mask_mobile("123")) == {"*"}

    def test_an_email_with_no_local_part_still_masks(self):
        assert mask_email("@example.com") == "***@example.com"

    def test_a_malformed_email_is_masked_entirely(self):
        # Not recognisable, so nothing about it can be assumed safe.
        assert set(mask_email("not-an-email")) == {"*"}

    def test_an_iban_with_spaces_is_handled(self):
        assert " " not in mask_iban("PK36 SCBL 0000 0011 2345 6702")


class TestRedactText:
    def test_it_masks_every_identifier_in_prose(self):
        text = (
            "Client Muhammad Ali, CNIC 35202-1234567-1, mobile 03001234567, "
            "email ali@example.com, account PK36SCBL0000001123456702."
        )

        redacted = redact_text(text)

        for secret in [
            "35202-1234567-1",
            "1234567",
            "ali@example.com",
            "PK36SCBL0000001123456702",
        ]:
            assert secret not in redacted, f"{secret} survived redaction"

    def test_it_leaves_ordinary_prose_alone(self):
        text = "The client asked about their filing deadline."

        assert redact_text(text) == text


class TestRestrictedGate:
    """The last check before egress."""

    def test_a_clean_payload_passes(self):
        assert_no_restricted({"document_type": "invoice", "confidence": 0.9})

    @pytest.mark.parametrize(
        "payload",
        [
            {"cnic": "35202-1234567-1"},
            {"fbr_password": "secret"},
            {"raw_text": "the whole document"},
            {"file_path": "/private/doc.pdf"},
        ],
    )
    def test_level_three_fields_are_refused(self, payload):
        # Raising is correct: dropping silently would let a caller believe the
        # field was sent, and sending it is the outcome the policy exists to stop.
        with pytest.raises(RestrictedDataError):
            assert_no_restricted(payload)

    def test_it_finds_restricted_data_nested_in_a_payload(self):
        with pytest.raises(RestrictedDataError, match="extracted.cnic"):
            assert_no_restricted({"ok": True, "extracted": {"cnic": "35202-1234567-1"}})

    def test_it_finds_restricted_data_inside_a_list(self):
        with pytest.raises(RestrictedDataError, match=r"documents\[1\]"):
            assert_no_restricted(
                {"documents": [{"type": "invoice"}, {"cnic": "35202-1234567-1"}]}
            )

    def test_the_error_names_the_offending_path(self):
        # A refusal that does not say where is a refusal nobody can act on.
        with pytest.raises(RestrictedDataError, match=r"payload\.client\.passport"):
            assert_no_restricted({"client": {"passport": "AB1234567"}})


class TestDataLevel:
    def test_levels_are_ordered(self):
        # Ordered so a comparison like `level >= DataLevel.REDACTED` is meaningful.
        assert DataLevel.SAFE < DataLevel.REDACTED < DataLevel.RESTRICTED
