"""Unit tests for auth helpers: _extract_dollar_config and UserInfo attribute mapping."""

from __future__ import annotations

import textwrap

import pytest

from isard.auth import _extract_dollar_config
from isard.errors import AzureFormParsingError
from isard.types import SamlAssertion
from datetime import datetime, timezone


# ---------------------------------------------------------------------------
# _extract_dollar_config
# ---------------------------------------------------------------------------


class TestExtractDollarConfig:
    def test_extracts_simple_config(self) -> None:
        html = 'var $Config={"urlPost":"/login","sFT":"tok123","canary":"abc"};'
        cfg = _extract_dollar_config(html)
        assert cfg["urlPost"] == "/login"
        assert cfg["sFT"] == "tok123"
        assert cfg["canary"] == "abc"

    def test_extracts_nested_config(self) -> None:
        html = textwrap.dedent("""\
            <script>
            $Config={"urlPost":"/path","nested":{"key":"val"},"sFT":"flow"};
            </script>
        """)
        cfg = _extract_dollar_config(html)
        assert cfg["sFT"] == "flow"
        assert cfg["nested"] == {"key": "val"}

    def test_handles_escaped_strings_in_values(self) -> None:
        # Escaped quotes inside a JSON string value must not confuse the brace walker
        html = r'$Config={"urlPost":"https://x.com/path?a=1\u0026b=2","sFT":"t"};'
        cfg = _extract_dollar_config(html)
        assert cfg["sFT"] == "t"

    def test_raises_when_marker_absent(self) -> None:
        with pytest.raises(AzureFormParsingError, match="Could not find"):
            _extract_dollar_config("<html>no config here</html>")

    def test_raises_on_invalid_json(self) -> None:
        html = "$Config={not valid json};"
        with pytest.raises(AzureFormParsingError, match="Failed to parse"):
            _extract_dollar_config(html)

    def test_handles_empty_object(self) -> None:
        html = "$Config={};"
        cfg = _extract_dollar_config(html)
        assert cfg == {}

    def test_config_preceded_by_other_text(self) -> None:
        html = 'other stuff\n$Config={"a":1};\nmore stuff'
        cfg = _extract_dollar_config(html)
        assert cfg["a"] == 1


# ---------------------------------------------------------------------------
# UserInfo attribute mapping (SamlAssertion.to_user_info)
# ---------------------------------------------------------------------------

_NS = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/"


def _make_assertion(attributes: dict) -> SamlAssertion:
    return SamlAssertion(
        id="_a1",
        issue_instant=datetime(2024, 1, 1, tzinfo=timezone.utc),
        issuer="https://sts.windows.net/test/",
        subject="subject@edu.gencat.cat",
        attributes=attributes,
    )


class TestUserInfoMapping:
    def test_maps_all_standard_attributes(self) -> None:
        attrs = {
            f"{_NS}userPrincipalName": ["user@edu.gencat.cat"],
            f"{_NS}DisplayName": ["Test User"],
            f"{_NS}emailaddress": ["user@edu.gencat.cat"],
            f"{_NS}givenname": ["Test"],
            f"{_NS}surname": ["User"],
            f"{_NS}CompanyName": ["Escola Test"],
            f"{_NS}Department": ["Teacher"],
        }
        ui = _make_assertion(attrs).to_user_info()
        assert ui.username == "user@edu.gencat.cat"
        assert ui.display_name == "Test User"
        assert ui.email == "user@edu.gencat.cat"
        assert ui.given_name == "Test"
        assert ui.family_name == "User"
        assert ui.organization == "Escola Test"
        assert ui.role == "Teacher"

    def test_falls_back_to_subject_when_no_upn(self) -> None:
        assertion = _make_assertion({})
        ui = assertion.to_user_info()
        assert ui.username == "subject@edu.gencat.cat"

    def test_display_name_falls_back_to_username(self) -> None:
        attrs = {f"{_NS}userPrincipalName": ["user@test.cat"]}
        ui = _make_assertion(attrs).to_user_info()
        assert ui.display_name == "user@test.cat"

    def test_email_falls_back_to_username(self) -> None:
        attrs = {f"{_NS}userPrincipalName": ["nomail@test.cat"]}
        ui = _make_assertion(attrs).to_user_info()
        assert ui.email == "nomail@test.cat"

    def test_optional_fields_are_none_when_absent(self) -> None:
        ui = _make_assertion({}).to_user_info()
        assert ui.given_name is None
        assert ui.family_name is None
        assert ui.organization is None
        assert ui.role is None

    def test_full_name_with_both_names(self) -> None:
        attrs = {
            f"{_NS}givenname": ["David"],
            f"{_NS}surname": ["Mingo"],
        }
        ui = _make_assertion(attrs).to_user_info()
        assert ui.full_name() == "David Mingo"

    def test_full_name_given_only(self) -> None:
        attrs = {f"{_NS}givenname": ["OnlyFirst"]}
        ui = _make_assertion(attrs).to_user_info()
        assert ui.full_name() == "OnlyFirst"

    def test_full_name_falls_back_to_display_name(self) -> None:
        attrs = {f"{_NS}DisplayName": ["Display Only"]}
        ui = _make_assertion(attrs).to_user_info()
        assert ui.full_name() == "Display Only"

    def test_saml_attributes_preserved(self) -> None:
        raw = {f"{_NS}userPrincipalName": ["u@x.cat"], "custom:attr": ["val"]}
        ui = _make_assertion(raw).to_user_info()
        assert ui.saml_attributes == raw
