# #region Test.AuthService [C:3] [TYPE Module] [SEMANTICS test,auth,service]
# @BRIEF Verify AuthService contracts — authenticate_user, create_session, provision_adfs_user.
# @RELATION BINDS_TO -> [AuthService]
# @TEST_EDGE: user_not_found -> authenticate_user returns None
# @TEST_EDGE: user_inactive -> authenticate_user returns None
# @TEST_EDGE: wrong_password -> authenticate_user returns None
# @TEST_EDGE: adfs_new_user -> provision_adfs_user creates User and commits
# @TEST_EDGE: adfs_existing_user -> provision_adfs_user syncs roles without creating
# @TEST_EDGE: adfs_no_upn_fallback_email -> username derived from email when upn absent

from pathlib import Path
import sys
from contextlib import contextmanager
from unittest.mock import MagicMock, call, patch

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

import pytest


# ── Helpers (C1 — anchor pair only) ──

# #region _make_user
def _make_user(**kwargs):
    defaults = {
        "id": "user-001",
        "username": "alice",
        "email": "alice@example.com",
        "password_hash": "$2b$12$FAKEHASH",
        "is_active": True,
        "roles": [],
        "last_login": None,
    }
    defaults.update(kwargs)
    user = MagicMock()
    for k, v in defaults.items():
        setattr(user, k, v)
    return user
# #endregion _make_user

# #region _make_role
def _make_role(name: str):
    role = MagicMock()
    role.name = name
    return role
# #endregion _make_role

# #region _noop_belief_scope
@contextmanager
def _noop_belief_scope(_id):
    yield
# #endregion _noop_belief_scope


# ── Fixtures ──

@pytest.fixture()
def mock_db():
    db = MagicMock()
    db.commit = MagicMock()
    db.refresh = MagicMock()
    db.add = MagicMock()
    return db


# ════════════════════════════════════════════════════════════════
# authenticate_user
# ════════════════════════════════════════════════════════════════

class TestAuthenticateUser:
    """authenticate_user — credential validation and account-state gating."""

    # #region test_authenticate_user_success
    # @BRIEF Active user with correct password returns User and updates last_login.
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.verify_password", return_value=True)
    @patch("src.services.auth_service.AuthRepository")
    def test_authenticate_user_success(self, MockRepo, _vp, _bs, mock_db):
        from src.services.auth_service import AuthService

        expected_user = _make_user(username="alice", is_active=True)
        instance = MockRepo.return_value
        instance.get_user_by_username.return_value = expected_user

        svc = AuthService(mock_db)
        result = svc.authenticate_user("alice", "correct-password")

        assert result is expected_user
        assert result.last_login is not None
        mock_db.commit.assert_called_once()
        mock_db.refresh.assert_called_once_with(expected_user)
    # #endregion test_authenticate_user_success

    # #region test_authenticate_user_not_found
    # @BRIEF Returns None when username does not exist in the database.
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.verify_password")
    @patch("src.services.auth_service.AuthRepository")
    def test_authenticate_user_not_found(self, MockRepo, mock_vp, _bs, mock_db):
        from src.services.auth_service import AuthService

        MockRepo.return_value.get_user_by_username.return_value = None

        svc = AuthService(mock_db)
        result = svc.authenticate_user("ghost", "pw")

        assert result is None
        mock_vp.assert_not_called()
    # #endregion test_authenticate_user_not_found

    # #region test_authenticate_user_inactive
    # @BRIEF Returns None when user exists but is_active is False.
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.verify_password")
    @patch("src.services.auth_service.AuthRepository")
    def test_authenticate_user_inactive(self, MockRepo, mock_vp, _bs, mock_db):
        from src.services.auth_service import AuthService

        inactive_user = _make_user(is_active=False)
        MockRepo.return_value.get_user_by_username.return_value = inactive_user

        svc = AuthService(mock_db)
        result = svc.authenticate_user("alice", "pw")

        assert result is None
        mock_vp.assert_not_called()
    # #endregion test_authenticate_user_inactive

    # #region test_authenticate_user_wrong_password
    # @BRIEF Returns None when password hash verification fails.
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.verify_password", return_value=False)
    @patch("src.services.auth_service.AuthRepository")
    def test_authenticate_user_wrong_password(self, MockRepo, _vp, _bs, mock_db):
        from src.services.auth_service import AuthService

        active_user = _make_user(is_active=True)
        MockRepo.return_value.get_user_by_username.return_value = active_user

        svc = AuthService(mock_db)
        result = svc.authenticate_user("alice", "wrong-password")

        assert result is None
        mock_db.commit.assert_not_called()
    # #endregion test_authenticate_user_wrong_password


# ════════════════════════════════════════════════════════════════
# create_session
# ════════════════════════════════════════════════════════════════

class TestCreateSession:
    """create_session — JWT issuance with role-based scopes."""

    # #region test_create_session_returns_bearer_token
    # @BRIEF Returns dict with access_token and token_type='bearer'.
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.create_access_token", return_value="jwt-token-abc")
    @patch("src.services.auth_service.AuthRepository")
    def test_create_session_returns_bearer_token(self, MockRepo, mock_jwt, _bs, mock_db):
        from src.services.auth_service import AuthService

        role_admin = _make_role("admin")
        role_editor = _make_role("editor")
        user = _make_user(username="alice", roles=[role_admin, role_editor])

        svc = AuthService(mock_db)
        session = svc.create_session(user)

        assert session == {"access_token": "jwt-token-abc", "token_type": "bearer"}
        mock_jwt.assert_called_once_with(
            data={"sub": "alice", "scopes": ["admin", "editor"]}
        )
    # #endregion test_create_session_returns_bearer_token

    # #region test_create_session_no_roles
    # @BRIEF User with no roles produces empty scopes list.
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.create_access_token", return_value="jwt-empty")
    @patch("src.services.auth_service.AuthRepository")
    def test_create_session_no_roles(self, MockRepo, mock_jwt, _bs, mock_db):
        from src.services.auth_service import AuthService

        user = _make_user(username="bob", roles=[])

        svc = AuthService(mock_db)
        session = svc.create_session(user)

        assert session["token_type"] == "bearer"
        mock_jwt.assert_called_once_with(data={"sub": "bob", "scopes": []})
    # #endregion test_create_session_no_roles


# ════════════════════════════════════════════════════════════════
# provision_adfs_user
# ════════════════════════════════════════════════════════════════

class TestProvisionAdfsUser:
    """provision_adfs_user — JIT provisioning and role synchronization."""

    # #region test_provision_new_adfs_user
    # @BRIEF Creates new User when username not found, sets ADFS fields, commits.
    @patch("src.services.auth_service.log_security_event")
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.AuthRepository")
    def test_provision_new_adfs_user(self, MockRepo, _bs, mock_log, mock_db):
        from src.services.auth_service import AuthService

        MockRepo.return_value.get_user_by_username.return_value = None
        mapped_role = _make_role("ad-users")
        MockRepo.return_value.get_roles_by_ad_groups.return_value = [mapped_role]

        svc = AuthService(mock_db)
        user_info = {"upn": "carol@corp.com", "email": "carol@corp.com", "name": "Carol", "groups": ["AD-Users"]}
        result = svc.provision_adfs_user(user_info)

        mock_db.add.assert_called_once()
        new_user = mock_db.add.call_args[0][0]
        assert new_user.username == "carol@corp.com"
        assert new_user.auth_source == "ADFS"
        assert new_user.is_ad_user is True
        assert new_user.is_active is True
        assert new_user.roles == [mapped_role]
        mock_db.commit.assert_called_once()
        mock_db.refresh.assert_called_once()
        mock_log.assert_called_once_with("USER_PROVISIONED", "carol@corp.com", {"source": "ADFS"})
    # #endregion test_provision_new_adfs_user

    # #region test_provision_existing_adfs_user
    # @BRIEF Existing user gets roles synced without creating a new record.
    @patch("src.services.auth_service.log_security_event")
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.AuthRepository")
    def test_provision_existing_adfs_user(self, MockRepo, _bs, mock_log, mock_db):
        from src.services.auth_service import AuthService

        existing = _make_user(username="dave@corp.com")
        MockRepo.return_value.get_user_by_username.return_value = existing
        role1 = _make_role("analysts")
        MockRepo.return_value.get_roles_by_ad_groups.return_value = [role1]

        svc = AuthService(mock_db)
        result = svc.provision_adfs_user({"upn": "dave@corp.com", "groups": ["AD-Analysts"]})

        mock_db.add.assert_not_called()
        mock_log.assert_not_called()
        assert existing.roles == [role1]
        assert existing.last_login is not None
        mock_db.commit.assert_called_once()
    # #endregion test_provision_existing_adfs_user

    # #region test_provision_adfs_user_email_fallback
    # @BRIEF Uses 'email' claim as username when 'upn' is absent.
    @patch("src.services.auth_service.log_security_event")
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.AuthRepository")
    def test_provision_adfs_user_email_fallback(self, MockRepo, _bs, mock_log, mock_db):
        from src.services.auth_service import AuthService

        MockRepo.return_value.get_user_by_username.return_value = None
        MockRepo.return_value.get_roles_by_ad_groups.return_value = []

        svc = AuthService(mock_db)
        result = svc.provision_adfs_user({"email": "eve@corp.com", "groups": []})

        new_user = mock_db.add.call_args[0][0]
        assert new_user.username == "eve@corp.com"
    # #endregion test_provision_adfs_user_email_fallback

    # #region test_provision_adfs_user_no_groups
    # @BRIEF Empty groups list results in empty roles and no error.
    @patch("src.services.auth_service.log_security_event")
    @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope)
    @patch("src.services.auth_service.AuthRepository")
    def test_provision_adfs_user_no_groups(self, MockRepo, _bs, mock_log, mock_db):
        from src.services.auth_service import AuthService

        existing = _make_user(username="frank@corp.com")
        MockRepo.return_value.get_user_by_username.return_value = existing
        MockRepo.return_value.get_roles_by_ad_groups.return_value = []

        svc = AuthService(mock_db)
        result = svc.provision_adfs_user({"upn": "frank@corp.com"})

        assert existing.roles == []
        mock_db.commit.assert_called_once()
    # #endregion test_provision_adfs_user_no_groups

# #endregion Test.AuthService
