"""SH-01: `tenant_session` SQL-injection boundary tests.

The implementation runs ``UUID(tenant_id)`` BEFORE constructing the
``SET LOCAL`` SQL string. The canonical UUID form is alphanumeric +
dashes — no SQL-meaningful characters — so once we've round-tripped
through `UUID()`, interpolation is safe.

This module pins that boundary against an adversarial corpus. Every
non-UUID input MUST raise `ValueError` BEFORE `session.execute` is
called; the mock session's `execute` recorder verifies the latter.

A future refactor that accepts a raw `str` without the `UUID(...)`
round-trip — even "to skip the validation cost" — fails the corpus.
That's the contract: the validation IS the security boundary.

(SH-01 of mgf-sqlalchemy's SECURITY_HARDENING tracker.)
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock
from uuid import UUID

import pytest

from mgf.sqlalchemy import tenant_session

# Adversarial inputs that MUST raise ValueError before any SQL.
# Categories cover SQL-meaningful characters, control bytes, unicode
# homoglyphs for digits/dashes, whitespace, empty/near-empty strings,
# and almost-valid-UUID strings.
_BAD_INPUTS = [
    # SQL meta-characters
    "'; DROP TABLE users; --",
    "abc'def",
    'abc"def',
    "abc;def",
    "abc/*comment*/def",
    "abc--comment\n",
    # Empty / whitespace
    "",
    "   ",
    "\n",
    "\t",
    # Control bytes
    "00000000-0000-0000-0000-00000000000\x00",
    "\x00\x00\x00\x00",
    # Almost-valid UUID shapes (wrong length, wrong chars)
    "12345678-1234-1234-1234-12345678901",  # one short
    "12345678-1234-1234-1234-1234567890123",  # one long
    "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ",  # non-hex
    "12345678_1234_1234_1234_123456789012",  # wrong separator
    # Unicode homoglyphs (digit/dash lookalikes)
    "12345678-1234-1234-1234-12345678901O",  # capital O instead of 0
    "12345678-1234-1234-1234-12345678901l",  # lowercase L instead of 1
    "12345678–1234-1234-1234-123456789012",  # noqa: RUF001 — U+2013 en-dash, intentional homoglyph test
    # Boolean false-positive shapes (string "None", "null", etc.)
    "None",
    "null",
    "false",
    "0",
    # Path-traversal-ish
    "../etc/passwd",
    "/etc/shadow",
]


def _build_mock_session() -> MagicMock:
    """Return a session-shaped mock whose `execute` records calls.

    The test asserts `execute` is NOT called on the invalid path.
    """
    session = MagicMock()
    session.execute = AsyncMock(return_value=None)
    return session


@pytest.mark.asyncio
class TestTenantSessionSqlInjectionBoundary:
    """SH-01: validate the UUID-validation boundary on adversarial input."""

    @pytest.mark.parametrize("bad_input", _BAD_INPUTS)
    async def test_invalid_input_raises_value_error_before_sql(self, bad_input: str) -> None:
        """Every adversarial input raises ValueError; execute never runs."""
        session = _build_mock_session()

        with pytest.raises(ValueError):
            async with tenant_session(session, bad_input):
                pass

        # The SQL-injection boundary is BEFORE the SET LOCAL emit.
        # If execute was called even once on an invalid path, the
        # validation order is wrong — a refactor regressed SH-INV-1.
        session.execute.assert_not_called()

    async def test_valid_uuid_str_does_run_sql(self) -> None:
        """Sanity: a valid UUID string DOES reach the SET LOCAL emit.

        Without this counter-test, an over-aggressive ValueError on
        every input would silently pass the boundary tests.
        """
        session = _build_mock_session()
        valid = "11111111-2222-3333-4444-555555555555"

        async with tenant_session(session, valid):
            pass

        session.execute.assert_called_once()
        call_args = session.execute.call_args
        sql_text = str(call_args.args[0])
        assert "11111111-2222-3333-4444-555555555555" in sql_text
        assert "SET LOCAL app.current_tenant" in sql_text

    async def test_valid_uuid_object_does_run_sql(self) -> None:
        """UUID object input bypasses the str-validation path but
        still reaches the SET LOCAL emit. Pins SH-INV-1's two-shape
        coverage."""
        session = _build_mock_session()
        valid = UUID("11111111-2222-3333-4444-555555555555")

        async with tenant_session(session, valid):
            pass

        session.execute.assert_called_once()

    async def test_canonical_form_used_regardless_of_input_case(self) -> None:
        """Different valid input shapes produce the same canonical
        SQL — `UUID()` normalises hex case + brace forms. Pins that
        the canonical form is what reaches the SQL, defeating any
        upper-vs-lowercase injection attempts."""
        session = _build_mock_session()
        valid_input = "{AABBCCDD-EEFF-1111-2222-333344445555}"

        async with tenant_session(session, valid_input):
            pass

        sql_text = str(session.execute.call_args.args[0])
        # Canonical: lowercase hex, no braces.
        assert "aabbccdd-eeff-1111-2222-333344445555" in sql_text
        assert "{" not in sql_text
        assert "}" not in sql_text


@pytest.mark.asyncio
class TestNilUuidPolicy:
    """RA-04 / SH-03: NIL UUID is ACCEPTED by `tenant_session`.

    The library does not reserve the NIL UUID for any special
    meaning. Consumers using it as a sentinel ("no tenant", "system
    rows") are responsible for the corresponding RLS policy
    handling — documented in the helper's docstring under "NIL UUID
    policy". This test pins the acceptance contract; a future
    behaviour change that rejects NIL fails this row, forcing a
    documented decision rather than a silent regression.
    """

    @pytest.mark.parametrize(
        "nil_input",
        [
            "00000000-0000-0000-0000-000000000000",  # canonical str
            "00000000000000000000000000000000",  # no-dash hex
            "{00000000-0000-0000-0000-000000000000}",  # brace form
            UUID("00000000-0000-0000-0000-000000000000"),  # UUID object
        ],
    )
    async def test_nil_uuid_accepted_across_input_shapes(self, nil_input: str | UUID) -> None:
        """All input shapes resolving to the NIL UUID are accepted.

        Pin: each form reaches the SET LOCAL emit with the canonical
        ``00000000-0000-0000-0000-000000000000`` interpolation. A
        future change that rejects NIL fails this test loudly.
        """
        session = _build_mock_session()

        async with tenant_session(session, nil_input):
            pass

        session.execute.assert_called_once()
        rendered = str(session.execute.call_args.args[0])
        assert "00000000-0000-0000-0000-000000000000" in rendered
        assert "SET LOCAL app.current_tenant" in rendered
