"""Unit tests for the SAML XML parser (saml.py)."""

from __future__ import annotations

import base64
import textwrap
from datetime import datetime, timezone

import pytest

from isard.errors import SamlParseError, SamlValidationError
from isard.saml import (
    parse_saml_response,
    parse_saml_response_base64,
    validate_response,
)
from isard.types import AuthSession, SamlResponse


# ---------------------------------------------------------------------------
# SAML XML fixture
# ---------------------------------------------------------------------------


def _make_saml_xml(
    *,
    status_code: str = "urn:oasis:names:tc:SAML:2.0:status:Success",
    response_id: str = "_resp001",
    not_before: str = "2020-01-01T00:00:00Z",
    not_on_or_after: str = "2099-12-31T23:59:59Z",
    session_not_on_or_after: str = "2099-12-31T23:59:59Z",
    destination: str = "https://elmeuescriptori.gestioeducativa.gencat.cat/authentication/saml/acs",
    subject: str = "ddemingo@edu.gencat.cat",
    upn: str = "ddemingo@edu.gencat.cat",
    display_name: str = "David de Mingo",
    email: str = "ddemingo@edu.gencat.cat",
    given_name: str = "David",
    family_name: str = "de Mingo",
    organization: str = "Escola Test",
    department: str = "Teacher",
) -> str:
    return textwrap.dedent(f"""\
        <?xml version="1.0" encoding="UTF-8"?>
        <samlp:Response
            xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
            xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
            ID="{response_id}"
            Version="2.0"
            IssueInstant="2024-01-01T12:00:00Z"
            Destination="{destination}"
            InResponseTo="_req001">
          <saml:Issuer>https://sts.windows.net/test-tenant/</saml:Issuer>
          <samlp:Status>
            <samlp:StatusCode Value="{status_code}"/>
          </samlp:Status>
          <saml:Assertion ID="_assert001" Version="2.0" IssueInstant="2024-01-01T12:00:00Z">
            <saml:Issuer>https://sts.windows.net/test-tenant/</saml:Issuer>
            <saml:Subject>
              <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">{subject}</saml:NameID>
              <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml:SubjectConfirmationData
                    NotOnOrAfter="{not_on_or_after}"
                    Recipient="{destination}"
                    InResponseTo="_req001"/>
              </saml:SubjectConfirmation>
            </saml:Subject>
            <saml:Conditions NotBefore="{not_before}" NotOnOrAfter="{not_on_or_after}">
              <saml:AudienceRestriction>
                <saml:Audience>https://elmeuescriptori.gestioeducativa.gencat.cat/authentication/saml/metadata</saml:Audience>
              </saml:AudienceRestriction>
            </saml:Conditions>
            <saml:AuthnStatement AuthnInstant="2024-01-01T12:00:00Z"
                SessionNotOnOrAfter="{session_not_on_or_after}"
                SessionIndex="_session001">
              <saml:AuthnContext>
                <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
              </saml:AuthnContext>
            </saml:AuthnStatement>
            <saml:AttributeStatement>
              <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/userPrincipalName">
                <saml:AttributeValue>{upn}</saml:AttributeValue>
              </saml:Attribute>
              <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/DisplayName">
                <saml:AttributeValue>{display_name}</saml:AttributeValue>
              </saml:Attribute>
              <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress">
                <saml:AttributeValue>{email}</saml:AttributeValue>
              </saml:Attribute>
              <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname">
                <saml:AttributeValue>{given_name}</saml:AttributeValue>
              </saml:Attribute>
              <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname">
                <saml:AttributeValue>{family_name}</saml:AttributeValue>
              </saml:Attribute>
              <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/CompanyName">
                <saml:AttributeValue>{organization}</saml:AttributeValue>
              </saml:Attribute>
              <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/Department">
                <saml:AttributeValue>{department}</saml:AttributeValue>
              </saml:Attribute>
            </saml:AttributeStatement>
          </saml:Assertion>
        </samlp:Response>
    """)


ACS_URL = "https://elmeuescriptori.gestioeducativa.gencat.cat/authentication/saml/acs"


# ---------------------------------------------------------------------------
# parse_saml_response
# ---------------------------------------------------------------------------


class TestParseSamlResponse:
    def test_parses_success_response(self) -> None:
        xml = _make_saml_xml()
        resp = parse_saml_response(xml)
        assert resp.id == "_resp001"
        assert resp.status.is_success()
        assert resp.issuer == "https://sts.windows.net/test-tenant/"
        assert resp.in_response_to == "_req001"
        assert resp.destination == ACS_URL

    def test_parses_assertion(self) -> None:
        xml = _make_saml_xml()
        resp = parse_saml_response(xml)
        assert resp.assertion is not None
        a = resp.assertion
        assert a.id == "_assert001"
        assert a.subject == "ddemingo@edu.gencat.cat"
        assert (
            a.name_id_format == "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
        )
        assert a.session_index == "_session001"
        assert a.authn_context_class_ref == (
            "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
        )
        assert ACS_URL.replace(
            "/authentication/saml/acs", "/authentication/saml/metadata"
        ) in (a.audience_restrictions)

    def test_parses_attributes(self) -> None:
        xml = _make_saml_xml(upn="user@edu.gencat.cat", display_name="Test User")
        resp = parse_saml_response(xml)
        assert resp.assertion is not None
        attrs = resp.assertion.attributes
        upn_vals = attrs.get(
            "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/userPrincipalName"
        )
        assert upn_vals == ["user@edu.gencat.cat"]
        dn_vals = attrs.get(
            "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/DisplayName"
        )
        assert dn_vals == ["Test User"]

    def test_parses_datetime_fields(self) -> None:
        xml = _make_saml_xml(
            not_before="2020-01-01T00:00:00Z",
            not_on_or_after="2099-12-31T23:59:59Z",
        )
        resp = parse_saml_response(xml)
        assert resp.assertion is not None
        assert resp.assertion.not_before is not None
        assert resp.assertion.not_before.tzinfo is not None
        assert resp.assertion.not_on_or_after is not None

    def test_raises_on_failed_status(self) -> None:
        xml = _make_saml_xml(
            status_code="urn:oasis:names:tc:SAML:2.0:status:AuthnFailed"
        )
        with pytest.raises(SamlValidationError, match="AuthnFailed"):
            parse_saml_response(xml)

    def test_raises_on_missing_id(self) -> None:
        xml = _make_saml_xml().replace('ID="_resp001"', "")
        with pytest.raises(SamlParseError, match="missing ID"):
            parse_saml_response(xml)

    def test_raises_on_invalid_xml(self) -> None:
        with pytest.raises(SamlParseError, match="XML parsing error"):
            parse_saml_response("<not-valid-xml")

    def test_raises_on_bad_base64(self) -> None:
        with pytest.raises(SamlParseError, match="base64"):
            parse_saml_response_base64("!!!not-base64!!!")

    def test_round_trip_base64(self) -> None:
        xml = _make_saml_xml()
        encoded = base64.b64encode(xml.encode()).decode()
        resp = parse_saml_response_base64(encoded)
        assert resp.status.is_success()
        assert resp.assertion is not None


# ---------------------------------------------------------------------------
# validate_response
# ---------------------------------------------------------------------------


class TestValidateResponse:
    def test_passes_valid_response(self) -> None:
        resp = parse_saml_response(_make_saml_xml())
        # Should not raise
        validate_response(resp, ACS_URL)

    def test_raises_on_destination_mismatch(self) -> None:
        resp = parse_saml_response(_make_saml_xml())
        with pytest.raises(SamlValidationError, match="destination mismatch"):
            validate_response(resp, "https://other.example.com/acs")

    def test_raises_on_expired_assertion(self) -> None:
        xml = _make_saml_xml(
            not_on_or_after="2000-01-01T00:00:00Z",
            session_not_on_or_after="2000-01-01T00:00:00Z",
        )
        resp = parse_saml_response(xml)
        with pytest.raises(SamlValidationError, match="expired"):
            validate_response(resp, ACS_URL, clock_skew_seconds=0)
