"""Unit tests for dta_utils_python.auth module."""

import base64
import json
import os
import time
import unittest
from unittest.mock import patch

import jwt
import requests
from dta_utils_python.auth import (
    AuthError,
    DTA_ISSUER,
    DTARoles,
    RAC_STUDIO_NAME_TO_DTA_ROLE,
    authorize,
    authorize_with_token,
    detect_jwt_type,
    get_identity_user_from_cache,
    is_identity_super_user,
    is_identity_token_expired,
    normalize_user_data,
    validate_dta_jwt,
    validate_identity_jwt,
)


def _b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).decode().rstrip("=")


def _make_fake_token(header: dict, payload: dict) -> str:
    """Token with header+payload; third segment is valid base64url so PyJWT parses it."""
    h = _b64url_encode(json.dumps(header, separators=(",", ":")).encode())
    p = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
    sig = _b64url_encode(b"fake_sig")
    return f"{h}.{p}.{sig}"


class TestConstants(unittest.TestCase):
    def test_dta_issuer(self):
        self.assertEqual(DTA_ISSUER, "dta.totvs.ai")

    def test_dtaroles_values(self):
        self.assertEqual(DTARoles.TENANT_ADMIN, "TENANT_ADMIN")
        self.assertEqual(DTARoles.TENANT_VIEWER, "TENANT_VIEWER")
        self.assertEqual(DTARoles.AGENT_USER, "AGENT_USER")

    def test_rac_studio_name_to_dta_role(self):
        self.assertEqual(
            RAC_STUDIO_NAME_TO_DTA_ROLE["DTA Studio Admin"],
            DTARoles.TENANT_ADMIN,
        )
        self.assertEqual(
            RAC_STUDIO_NAME_TO_DTA_ROLE["DTA Studio Viewer"],
            DTARoles.TENANT_VIEWER,
        )
        self.assertEqual(
            RAC_STUDIO_NAME_TO_DTA_ROLE["DTA Studio User"],
            DTARoles.AGENT_USER,
        )


class TestDetectJwtType(unittest.TestCase):
    def test_detect_dta_jwt(self):
        token = jwt.encode(
            {"sub": "user", "iss": DTA_ISSUER},
            "secret",
            algorithm="HS256",
        )
        self.assertEqual(detect_jwt_type(token), "dta")
        self.assertEqual(detect_jwt_type("Bearer " + token), "dta")

    def test_detect_identity_jwt(self):
        token = _make_fake_token(
            {"alg": "RS256", "kid": "test-key", "typ": "JWT"},
            {"sub": "user"},
        )
        self.assertEqual(detect_jwt_type(token), "identity")

    def test_detect_invalid_token_raises(self):
        with self.assertRaises(AuthError) as ctx:
            detect_jwt_type("not-a-jwt")
        self.assertEqual(ctx.exception.status_code, 401)
        self.assertIn("Invalid", ctx.exception.detail)

    def test_detect_unsupported_alg_raises(self):
        token = _make_fake_token(
            {"alg": "HS384", "typ": "JWT"},
            {"sub": "x"},
        )
        with self.assertRaises(AuthError) as ctx:
            detect_jwt_type(token)
        self.assertEqual(ctx.exception.status_code, 401)


class TestValidateDtaJwt(unittest.TestCase):
    def test_valid_dta_jwt(self):
        payload = {"sub": "u", "iss": DTA_ISSUER, "exp": time.time() + 3600}
        token = jwt.encode(payload, "my-secret", algorithm="HS256")
        result = validate_dta_jwt(token, "my-secret")
        self.assertEqual(result["iss"], DTA_ISSUER)
        self.assertEqual(result["sub"], "u")

    def test_expired_dta_jwt_raises(self):
        payload = {"sub": "u", "iss": DTA_ISSUER, "exp": time.time() - 10}
        token = jwt.encode(payload, "my-secret", algorithm="HS256")
        with self.assertRaises(AuthError) as ctx:
            validate_dta_jwt(token, "my-secret")
        self.assertEqual(ctx.exception.status_code, 401)
        self.assertIn("expired", ctx.exception.detail.lower())

    def test_wrong_issuer_raises(self):
        payload = {"sub": "u", "iss": "other.issuer", "exp": time.time() + 3600}
        token = jwt.encode(payload, "my-secret", algorithm="HS256")
        with self.assertRaises(AuthError) as ctx:
            validate_dta_jwt(token, "my-secret", issuer=DTA_ISSUER)
        self.assertEqual(ctx.exception.status_code, 401)

    def test_wrong_secret_raises(self):
        payload = {"sub": "u", "iss": DTA_ISSUER, "exp": time.time() + 3600}
        token = jwt.encode(payload, "my-secret", algorithm="HS256")
        with self.assertRaises(AuthError) as ctx:
            validate_dta_jwt(token, "wrong-secret")
        self.assertEqual(ctx.exception.status_code, 401)

    def test_empty_secret_raises_500(self):
        token = jwt.encode(
            {"iss": DTA_ISSUER, "exp": time.time() + 3600},
            "x",
            algorithm="HS256",
        )
        with self.assertRaises(AuthError) as ctx:
            validate_dta_jwt(token, "")
        self.assertEqual(ctx.exception.status_code, 500)


class TestNormalizeUserData(unittest.TestCase):
    def test_normalize_dta_payload(self):
        payload = {
            "name": "Test User",
            "identifier": "id-123",
            "organization": "org1",
            "super": True,
            "org_admin": False,
            "tenants": ["t1"],
            "roles": [{"tenant": "t1", "role": "TENANT_ADMIN"}],
            "exp": 12345,
            "generated_at": "2020-01-01",
        }
        out = normalize_user_data(payload, "dta")
        self.assertEqual(out["name"], "Test User")
        self.assertEqual(out["identifier"], "id-123")
        self.assertEqual(out["email"], "id-123")
        self.assertEqual(out["organization"], "org1")
        self.assertTrue(out["super"])
        self.assertEqual(out["jwt_type"], "dta")
        self.assertEqual(out["generated_at"], "2020-01-01")

    def test_normalize_identity_payload(self):
        payload = {
            "fullName": "Identity User",
            "email": "user@example.com",
            "authorities": ["admin", "user"],
            "apps": [],
            "companyId": "c1",
            "tenantCode": "t1",
        }
        out = normalize_user_data(payload, "identity")
        self.assertEqual(out["name"], "Identity User")
        self.assertEqual(out["identifier"], "user@example.com")
        self.assertEqual(out["email"], "user@example.com")
        self.assertEqual(out["organization"], "totvs")
        self.assertTrue(out["super"])
        self.assertTrue(out["org_admin"])
        self.assertEqual(out["jwt_type"], "identity")
        self.assertEqual(out["authorities"], ["admin", "user"])

    def test_normalize_unsupported_type_raises(self):
        with self.assertRaises(ValueError):
            normalize_user_data({}, "unknown")


class TestIsIdentityTokenExpired(unittest.TestCase):
    def test_no_exp_not_expired(self):
        self.assertFalse(is_identity_token_expired({}))
        self.assertFalse(is_identity_token_expired({"email": "x"}))

    def test_exp_in_future_not_expired(self):
        self.assertFalse(is_identity_token_expired({"exp": time.time() + 3600}))

    def test_exp_in_past_expired(self):
        self.assertTrue(is_identity_token_expired({"exp": time.time() - 10}))

    def test_exp_invalid_returns_false(self):
        self.assertFalse(is_identity_token_expired({"exp": "not-a-number"}))


class TestAuthError(unittest.TestCase):
    def test_auth_error_attributes(self):
        err = AuthError("Bad token", status_code=401)
        self.assertEqual(err.detail, "Bad token")
        self.assertEqual(err.status_code, 401)
        self.assertIn("Bad token", str(err))


class TestValidateIdentityJwt(unittest.TestCase):
    def test_identity_jwt_missing_kid_raises(self):
        token = _make_fake_token(
            {"alg": "RS256", "typ": "JWT"},
            {"sub": "u", "exp": 9999999999},
        )
        with self.assertRaises(AuthError) as ctx:
            validate_identity_jwt(
                token,
                jwks_base_url="https://example.com/jwks",
            )
        self.assertEqual(ctx.exception.status_code, 401)
        self.assertIn("Kid", ctx.exception.detail)

    @patch("dta_utils_python.auth.jwt.requests.get")
    def test_identity_jwt_jwks_request_failure_raises(self, mock_get):
        mock_get.side_effect = requests.exceptions.RequestException("Connection error")
        token = _make_fake_token(
            {"alg": "RS256", "kid": "test-key", "typ": "JWT"},
            {"sub": "u", "exp": 9999999999},
        )
        with self.assertRaises(AuthError) as ctx:
            validate_identity_jwt(
                token,
                jwks_base_url="https://example.com/jwks",
            )
        self.assertEqual(ctx.exception.status_code, 500)
        self.assertIn("Connection", ctx.exception.detail)

    @patch("dta_utils_python.auth.jwt.get_jwks_public_key")
    def test_identity_jwt_empty_keys_in_jwks_raises(self, mock_get_key):
        mock_get_key.side_effect = AuthError("No keys found", status_code=401)
        token = _make_fake_token(
            {"alg": "RS256", "kid": "test-key", "typ": "JWT"},
            {"sub": "u", "exp": 9999999999},
        )
        with self.assertRaises(AuthError) as ctx:
            validate_identity_jwt(
                token,
                jwks_base_url="https://example.com/jwks",
            )
        self.assertEqual(ctx.exception.status_code, 401)


class TestGetIdentityUserFromCache(unittest.TestCase):
    """Tests for get_identity_user_from_cache (shared Redis JWT cache)."""

    @patch.dict(
        "os.environ",
        {
            "DTA_ENVIRONMENT": "development",
            "DTA_TENANT_NAME": "dta-teste",
            "DTA_TOTVS_TENANT_ID": "1a2b3c-4d5e6f",
            "DTA_ROLE_USER": "TENANT_ADMIN",
        },
        clear=False,
    )
    @patch("dta_utils_python.auth.cache.get_jwt_cached")
    @patch("dta_utils_python.auth.cache.delete_jwt_cached")
    @patch("dta_utils_python.auth.jwt.validate_identity_jwt")
    def test_local_dev_env_override_validates_jwt_and_uses_env_role(
        self, mock_validate, mock_del, mock_get
    ):
        mock_validate.return_value = {
            "fullName": "Local User",
            "email": "local@example.com",
            "exp": time.time() + 3600,
        }

        result = get_identity_user_from_cache("token123")
        self.assertEqual(result["identifier"], "local@example.com")
        self.assertEqual(result["name"], "Local User")
        self.assertEqual(
            result["dta_roles"],
            [
                {
                    "tenant": "1a2b3c-4d5e6f",
                    "tenant_id": "1a2b3c-4d5e6f",
                        "tenant_name": "dta-teste",
                    "role": "TENANT_ADMIN",
                }
            ],
        )
        mock_get.assert_not_called()
        mock_del.assert_not_called()
        mock_validate.assert_called_once()

    @patch("dta_utils_python.auth.cache.get_jwt_cached")
    @patch("dta_utils_python.auth.cache.delete_jwt_cached")
    def test_returns_normalized_dict_when_valid(self, mock_del, mock_get):
        mock_del.return_value = True
        mock_get.return_value = {
            "name": "Jane",
            "email": "jane@example.com",
            "dta_roles": [{"tenant": "t1", "role": "TENANT_ADMIN"}],
            "exp": time.time() + 3600,
            "org": "myorg",
        }
        result = get_identity_user_from_cache("token123")
        self.assertEqual(result["name"], "Jane")
        self.assertEqual(result["identifier"], "jane@example.com")
        self.assertEqual(
            result["dta_roles"], [{"tenant": "t1", "role": "TENANT_ADMIN"}]
        )
        self.assertEqual(result["org"], "myorg")
        self.assertFalse(result["super"])
        mock_get.assert_called_once_with("token123", _redis=None)
        mock_del.assert_not_called()

    @patch("dta_utils_python.auth.cache.get_jwt_cached")
    @patch("dta_utils_python.auth.cache.delete_jwt_cached")
    def test_returns_super_when_in_cache(self, mock_del, mock_get):
        mock_del.return_value = True
        mock_get.return_value = {
            "name": "Super",
            "email": "super@example.com",
            "dta_roles": [],
            "exp": time.time() + 3600,
            "org": "totvs",
            "super": True,
        }
        result = get_identity_user_from_cache("token456")
        self.assertTrue(result["super"])

    @patch("dta_utils_python.auth.cache.get_jwt_cached")
    @patch("dta_utils_python.auth.cache.delete_jwt_cached")
    def test_raises_when_not_in_cache(self, mock_del, mock_get):
        mock_get.return_value = None
        mock_del.return_value = True
        with self.assertRaises(AuthError) as ctx:
            get_identity_user_from_cache("token123")
        self.assertEqual(ctx.exception.status_code, 401)
        self.assertIn("not found", ctx.exception.detail.lower())
        mock_del.assert_called_once_with("token123", _redis=None)

    @patch("dta_utils_python.auth.cache.get_jwt_cached")
    @patch("dta_utils_python.auth.cache.delete_jwt_cached")
    def test_raises_when_expired(self, mock_del, mock_get):
        mock_get.return_value = {
            "name": "x",
            "email": "x@x.com",
            "dta_roles": [],
            "exp": time.time() - 60,
            "org": "totvs",
        }
        mock_del.return_value = True
        with self.assertRaises(AuthError) as ctx:
            get_identity_user_from_cache("token123")
        self.assertEqual(ctx.exception.status_code, 401)
        self.assertIn("expired", ctx.exception.detail.lower())
        mock_del.assert_called_once_with("token123", _redis=None)


class TestAuthorize(unittest.TestCase):
    """Tests for centralized authorize() and is_identity_super_user()."""

    def test_identity_super_user_true(self):
        payload = {"super": True, "dta_roles": []}
        self.assertTrue(is_identity_super_user(payload))

    def test_identity_super_user_false_no_super(self):
        payload = {"super": False, "dta_roles": []}
        self.assertFalse(is_identity_super_user(payload))

    def test_identity_super_user_false_no_dta_roles_key(self):
        payload = {"super": True}
        self.assertFalse(is_identity_super_user(payload))

    def test_authorize_identity_god_mode(self):
        payload = {"super": True, "dta_roles": [], "org": "totvs"}
        self.assertFalse(
            authorize(payload, "totvs", "any-tenant", [DTARoles.TENANT_ADMIN], "identity")
        )

    def test_authorize_identity_super_bypasses_role_check_with_matching_tenant(self):
        payload = {
            "super": True,
            "dta_roles": [{"tenant": "proj1", "role": "SOME_ROLE"}],
            "org": "totvs",
        }
        self.assertTrue(
            authorize(payload, "totvs", "proj1", [DTARoles.TENANT_ADMIN], "identity")
        )

    def test_authorize_identity_tenant_admin_ok(self):
        payload = {
            "super": False,
            "dta_roles": [{"tenant": "proj1", "role": "TENANT_ADMIN"}],
            "org": "totvs",
        }
        self.assertTrue(
            authorize(payload, "totvs", "proj1", [DTARoles.TENANT_ADMIN], "identity")
        )

    def test_authorize_identity_wrong_tenant_false(self):
        payload = {
            "super": False,
            "dta_roles": [{"tenant": "proj1", "role": "TENANT_ADMIN"}],
            "org": "totvs",
        }
        self.assertFalse(
            authorize(payload, "totvs", "other-proj", [DTARoles.TENANT_ADMIN], "identity")
        )

    def test_authorize_identity_org_mismatch_false(self):
        payload = {
            "super": False,
            "dta_roles": [{"tenant": "proj1", "role": "TENANT_ADMIN"}],
            "org": "acme",
        }
        self.assertFalse(
            authorize(payload, "totvs", "proj1", [DTARoles.TENANT_ADMIN], "identity")
        )

    def test_authorize_dta_super_ok(self):
        payload = {
            "super": True,
            "organization": "totvs",
            "roles": [{"tenant": "proj1", "role": "TENANT_ADMIN", "rights": {}}],
        }
        self.assertTrue(
            authorize(payload, "totvs", "proj1", [DTARoles.TENANT_ADMIN], "dta")
        )

    def test_authorize_dta_super_bypasses_role_check_with_matching_tenant(self):
        payload = {
            "super": True,
            "organization": "totvs",
            "roles": [{"tenant": "proj1", "role": "SOME_ROLE", "rights": {}}],
        }
        self.assertTrue(
            authorize(payload, "totvs", "proj1", [DTARoles.TENANT_ADMIN], "dta")
        )

    def test_authorize_dta_is_super_ok(self):
        payload = {
            "is_super": True,
            "organization": "totvs",
            "roles": [],
        }
        self.assertFalse(
            authorize(payload, "totvs", "proj1", [DTARoles.TENANT_ADMIN], "dta")
        )

    def test_authorize_dta_tenant_admin_by_role_ok(self):
        payload = {
            "super": False,
            "organization": "totvs",
            "roles": [{"tenant": "proj1", "role": "TENANT_ADMIN", "rights": {}}],
        }
        self.assertTrue(
            authorize(payload, "totvs", "proj1", [DTARoles.TENANT_ADMIN], "dta")
        )

    def test_authorize_dta_proxy_admin_maps_to_tenant_admin(self):
        payload = {
            "super": False,
            "organization": "totvs",
            "roles": [
                {"tenant": "proj1", "role": "SOME_ROLE", "rights": {"proxy": "ADMIN"}}
            ],
        }
        self.assertTrue(
            authorize(payload, "totvs", "proj1", [DTARoles.TENANT_ADMIN], "dta")
        )

    def test_authorize_dta_org_admin_no_tenant_ok(self):
        payload = {"super": False, "org_admin": True, "organization": "totvs", "roles": []}
        self.assertTrue(
            authorize(payload, "totvs", None, [DTARoles.ORG_ADMIN, DTARoles.TENANT_ADMIN], "dta")
        )

    def test_authorize_empty_required_roles_ok(self):
        payload = {"super": False, "dta_roles": [], "org": "totvs"}
        self.assertTrue(authorize(payload, "totvs", "proj1", [], "identity"))

    def test_authorize_invalid_jwt_type_false(self):
        payload = {"super": True, "dta_roles": []}
        self.assertFalse(
            authorize(payload, "totvs", "proj1", [DTARoles.TENANT_ADMIN], "unknown")
        )


class TestAuthorizeWithToken(unittest.TestCase):
    """authorize_with_token uses cache + jwt internally."""

    @patch("dta_utils_python.auth.authorization.get_identity_user_from_cache")
    @patch("dta_utils_python.auth.authorization.detect_jwt_type")
    def test_identity_super_via_token_authorized(self, mock_type, mock_cache):
        mock_type.return_value = "identity"
        mock_cache.return_value = {
            "super": True,
            "dta_roles": [],
            "org": "totvs",
        }
        authorized, payload, jwt_type = authorize_with_token(
            "Bearer identity-token-xyz",
            [DTARoles.TENANT_ADMIN],
            org="totvs",
            tenant="proj1",
        )
        self.assertFalse(authorized)
        self.assertIsNone(payload)
        self.assertEqual(jwt_type, "identity")
        mock_type.assert_called_once()
        mock_cache.assert_called_once()

    @patch("dta_utils_python.auth.authorization.validate_dta_jwt")
    @patch("dta_utils_python.auth.authorization.detect_jwt_type")
    def test_dta_super_via_token_authorized(self, mock_type, mock_validate):
        mock_type.return_value = "dta"
        mock_validate.return_value = {
            "super": True,
            "organization": "totvs",
            "roles": [],
        }
        with patch.dict(os.environ, {"JWT_SECRET": "test-secret"}):
            authorized, payload, jwt_type = authorize_with_token(
                "dta-token-xyz",
                [DTARoles.TENANT_ADMIN],
                org="totvs",
                tenant="proj1",
            )
        self.assertFalse(authorized)
        self.assertIsNone(payload)
        self.assertEqual(jwt_type, "dta")
        mock_validate.assert_called_once()

    def test_empty_token_raises(self):
        with self.assertRaises(AuthError) as ctx:
            authorize_with_token("", [DTARoles.TENANT_ADMIN])
        self.assertEqual(ctx.exception.status_code, 401)
