# tests/unit/services/test_store_team_service.py
"""Unit tests for StoreTeamService."""

import uuid
from datetime import datetime, timedelta

import pytest

from app.modules.tenancy.exceptions import (
    CannotRemoveOwnerException,
    InvalidInvitationTokenException,
    InvalidRoleException,
    UserNotFoundException,
)
from app.modules.tenancy.models import (
    Platform,
    Role,
    Store,
    StorePlatform,
    StoreUser,
    User,
)
from app.modules.tenancy.services.store_team_service import store_team_service

# =============================================================================
# FIXTURES
# =============================================================================


@pytest.fixture
def team_store(db, test_merchant):
    """Create a store for team tests."""
    unique_id = str(uuid.uuid4())[:8]
    store = Store(
        merchant_id=test_merchant.id,
        store_code=f"TEAMSTORE_{unique_id.upper()}",
        subdomain=f"teamstore{unique_id.lower()}",
        name=f"Team Store {unique_id}",
        is_active=True,
        is_verified=True,
    )
    db.add(store)
    db.commit()
    db.refresh(store)
    return store


@pytest.fixture
def store_owner(db, team_store, test_user):
    """Create an owner for the team store."""
    store_user = StoreUser(
        store_id=team_store.id,
        user_id=test_user.id,
        is_active=True,
    )
    db.add(store_user)
    db.commit()
    db.refresh(store_user)
    return store_user


@pytest.fixture
def team_member(db, team_store, other_user, auth_manager):
    """Create a team member for the store."""
    # Create a role first
    role = Role(
        store_id=team_store.id,
        name="staff",
        permissions=["orders.view", "products.view"],
    )
    db.add(role)
    db.commit()

    store_user = StoreUser(
        store_id=team_store.id,
        user_id=other_user.id,
        role_id=role.id,
        is_active=True,
        invitation_accepted_at=datetime.utcnow(),
    )
    db.add(store_user)
    db.commit()
    db.refresh(store_user)
    return store_user


@pytest.fixture
def pending_invitation(db, team_store, test_user, auth_manager):
    """Create a pending invitation."""
    unique_id = str(uuid.uuid4())[:8]

    # Create new user for invitation
    new_user = User(
        email=f"pending_{unique_id}@example.com",
        username=f"pending_{unique_id}",
        hashed_password=auth_manager.hash_password("temppass"),  # noqa: SEC001
        role="store_member",
        is_active=False,
    )
    db.add(new_user)
    db.commit()

    # Create role
    role = Role(
        store_id=team_store.id,
        name="support",
        permissions=["support.view"],
    )
    db.add(role)
    db.commit()

    # Create pending store user
    store_user = StoreUser(
        store_id=team_store.id,
        user_id=new_user.id,
        role_id=role.id,
        invited_by=test_user.id,
        invitation_token=f"pending_token_{unique_id}",
        invitation_sent_at=datetime.utcnow(),
        is_active=False,
    )
    db.add(store_user)
    db.commit()
    db.refresh(store_user)
    return store_user


@pytest.fixture
def expired_invitation(db, team_store, test_user, auth_manager):
    """Create an expired invitation."""
    unique_id = str(uuid.uuid4())[:8]

    # Create new user for invitation
    new_user = User(
        email=f"expired_{unique_id}@example.com",
        username=f"expired_{unique_id}",
        hashed_password=auth_manager.hash_password("temppass"),  # noqa: SEC001
        role="store_member",
        is_active=False,
    )
    db.add(new_user)
    db.commit()

    # Create role
    role = Role(
        store_id=team_store.id,
        name="viewer",
        permissions=["read_only"],
    )
    db.add(role)
    db.commit()

    # Create expired store user (sent 8 days ago, expires in 7)
    store_user = StoreUser(
        store_id=team_store.id,
        user_id=new_user.id,
        role_id=role.id,
        invited_by=test_user.id,
        invitation_token=f"expired_token_{unique_id}",
        invitation_sent_at=datetime.utcnow() - timedelta(days=8),
        is_active=False,
    )
    db.add(store_user)
    db.commit()
    db.refresh(store_user)
    return store_user


# =============================================================================
# INVITE TEAM MEMBER TESTS
# =============================================================================



# TestStoreTeamServiceInvite removed — check_team_limit was refactored in SubscriptionService


# =============================================================================
# ACCEPT INVITATION TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestStoreTeamServiceAccept:
    """Test suite for accepting invitations."""

    def test_accept_invitation_success(self, db, pending_invitation):
        """Test accepting a valid invitation."""
        result = store_team_service.accept_invitation(
            db=db,
            invitation_token=pending_invitation.invitation_token,
            password="newpassword123",  # noqa: SEC001
            first_name="John",
            last_name="Doe",
        )
        db.commit()

        assert result is not None
        assert result["user"].is_active is True
        assert result["user"].first_name == "John"
        assert result["user"].last_name == "Doe"

    def test_accept_invitation_invalid_token(self, db):
        """Test accepting with invalid token raises exception."""
        with pytest.raises(InvalidInvitationTokenException):
            store_team_service.accept_invitation(
                db=db,
                invitation_token="invalid_token_12345",
                password="password123",  # noqa: SEC001
            )

    def test_accept_invitation_already_accepted(self, db, team_member):
        """Test accepting an already accepted invitation raises exception."""
        # team_member already has invitation_accepted_at set
        with pytest.raises(InvalidInvitationTokenException):
            store_team_service.accept_invitation(
                db=db,
                invitation_token="some_token",  # team_member has no token
                password="password123",  # noqa: SEC001
            )

    def test_accept_invitation_expired(self, db, expired_invitation):
        """Test accepting an expired invitation raises exception."""
        with pytest.raises(InvalidInvitationTokenException) as exc_info:
            store_team_service.accept_invitation(
                db=db,
                invitation_token=expired_invitation.invitation_token,
                password="password123",  # noqa: SEC001
            )

        assert "expired" in str(exc_info.value).lower()


# =============================================================================
# REMOVE TEAM MEMBER TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestStoreTeamServiceRemove:
    """Test suite for removing team members."""

    def test_remove_team_member_success(self, db, team_store, team_member):
        """Test removing a team member (soft delete)."""
        result = store_team_service.remove_team_member(
            db=db,
            store=team_store,
            user_id=team_member.user_id,
        )
        db.commit()

        # Verify soft-deleted (hidden from normal queries, visible with include_deleted)
        from app.modules.tenancy.models import StoreUser

        hidden = db.query(StoreUser).filter(StoreUser.id == team_member.id).first()
        assert hidden is None  # Filtered out by soft-delete

        visible = (
            db.query(StoreUser)
            .execution_options(include_deleted=True)
            .filter(StoreUser.id == team_member.id)
            .first()
        )
        assert visible is not None
        assert visible.deleted_at is not None
        assert result is True

    def test_remove_owner_raises_error(self, db, team_store, store_owner):
        """Test removing owner raises exception."""
        with pytest.raises(CannotRemoveOwnerException):
            store_team_service.remove_team_member(
                db=db,
                store=team_store,
                user_id=store_owner.user_id,
            )

    def test_remove_nonexistent_user_raises_error(self, db, team_store):
        """Test removing non-existent user raises exception."""
        with pytest.raises(UserNotFoundException):
            store_team_service.remove_team_member(
                db=db,
                store=team_store,
                user_id=99999,
            )


# =============================================================================
# UPDATE MEMBER ROLE TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestStoreTeamServiceUpdateRole:
    """Test suite for updating member roles."""

    def test_update_role_success(self, db, team_store, team_member):
        """Test updating a member's role."""
        result = store_team_service.update_member_role(
            db=db,
            store=team_store,
            user_id=team_member.user_id,
            new_role_name="manager",
        )
        db.commit()

        assert result is not None
        assert result.role.name == "manager"

    def test_update_owner_role_raises_error(self, db, team_store, store_owner):
        """Test updating owner's role raises exception."""
        with pytest.raises(CannotRemoveOwnerException):
            store_team_service.update_member_role(
                db=db,
                store=team_store,
                user_id=store_owner.user_id,
                new_role_name="staff",
            )

    def test_update_role_with_custom_permissions(self, db, team_store, team_member):
        """Test updating role with custom permissions."""
        custom_perms = ["orders.view", "orders.edit", "reports.view"]

        result = store_team_service.update_member_role(
            db=db,
            store=team_store,
            user_id=team_member.user_id,
            new_role_name="analyst",
            custom_permissions=custom_perms,
        )
        db.commit()

        assert result.role.name == "analyst"
        assert result.role.permissions == custom_perms

    def test_update_nonexistent_user_raises_error(self, db, team_store):
        """Test updating non-existent user raises exception."""
        with pytest.raises(UserNotFoundException):
            store_team_service.update_member_role(
                db=db,
                store=team_store,
                user_id=99999,
                new_role_name="staff",
            )


# =============================================================================
# GET TEAM MEMBERS TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestStoreTeamServiceGetMembers:
    """Test suite for getting team members."""

    def test_get_team_members(self, db, team_store, store_owner, team_member):
        """Test getting all team members."""
        members = store_team_service.get_team_members(db, team_store)

        assert len(members) >= 2
        user_ids = [m["id"] for m in members]
        assert store_owner.user_id in user_ids
        assert team_member.user_id in user_ids

    def test_get_team_members_excludes_inactive(
        self, db, team_store, store_owner, team_member
    ):
        """Test getting only active team members."""
        # Deactivate team_member
        team_member.is_active = False
        db.commit()

        members = store_team_service.get_team_members(
            db, team_store, include_inactive=False
        )

        user_ids = [m["id"] for m in members]
        assert store_owner.user_id in user_ids
        assert team_member.user_id not in user_ids

    def test_get_team_members_includes_inactive(
        self, db, team_store, store_owner, team_member
    ):
        """Test getting all members including inactive."""
        # Deactivate team_member
        team_member.is_active = False
        db.commit()

        members = store_team_service.get_team_members(
            db, team_store, include_inactive=True
        )

        user_ids = [m["id"] for m in members]
        assert store_owner.user_id in user_ids
        assert team_member.user_id in user_ids


# =============================================================================
# GET STORE ROLES TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestStoreTeamServiceGetRoles:
    """Test suite for getting store roles."""

    def test_get_store_roles_existing(self, db, team_store, team_member):
        """Test getting roles when they exist."""
        # team_member fixture creates a role
        roles = store_team_service.get_store_roles(db, team_store.id)

        assert len(roles) >= 1
        role_names = [r["name"] for r in roles]
        assert "staff" in role_names

    def test_get_store_roles_creates_defaults(self, db, team_store):
        """Test default roles are created if none exist."""
        roles = store_team_service.get_store_roles(db, team_store.id)
        db.commit()

        assert len(roles) >= 5  # Default roles
        role_names = [r["name"] for r in roles]
        assert "manager" in role_names
        assert "staff" in role_names
        assert "support" in role_names
        assert "viewer" in role_names
        assert "marketing" in role_names

    def test_get_store_roles_returns_permissions(self, db, team_store):
        """Test roles include permissions."""
        roles = store_team_service.get_store_roles(db, team_store.id)

        for role in roles:
            assert "permissions" in role
            assert isinstance(role["permissions"], list)


# =============================================================================
# CUSTOM ROLE CRUD TESTS
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestStoreTeamServiceCreateCustomRole:
    """Test suite for creating custom roles."""

    def test_create_custom_role_success(self, db, team_store, test_user):
        """Test creating a custom role with valid permissions."""
        role = store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="content_editor",
            permissions=["products.view", "products.edit"],
            actor_user_id=test_user.id,
        )
        db.commit()

        assert role is not None
        assert role.name == "content_editor"
        assert role.store_id == team_store.id
        assert "products.view" in role.permissions
        assert "products.edit" in role.permissions

    def test_create_role_with_preset_name_raises_error(self, db, team_store, test_user):
        """Test creating a role with a preset name raises ValueError."""
        with pytest.raises(InvalidRoleException, match="preset name"):
            store_team_service.create_custom_role(
                db=db,
                store_id=team_store.id,
                name="manager",
                permissions=["products.view"],
                actor_user_id=test_user.id,
            )

    def test_create_role_with_preset_name_case_insensitive(self, db, team_store, test_user):
        """Test preset name check is case-insensitive."""
        with pytest.raises(InvalidRoleException, match="preset name"):
            store_team_service.create_custom_role(
                db=db,
                store_id=team_store.id,
                name="Manager",
                permissions=["products.view"],
                actor_user_id=test_user.id,
            )

    def test_create_duplicate_role_name_raises_error(self, db, team_store, test_user):
        """Test creating a role with duplicate name raises ValueError."""
        store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="editor",
            permissions=["products.view"],
            actor_user_id=test_user.id,
        )
        db.flush()

        with pytest.raises(InvalidRoleException, match="already exists"):
            store_team_service.create_custom_role(
                db=db,
                store_id=team_store.id,
                name="editor",
                permissions=["orders.view"],
                actor_user_id=test_user.id,
            )

    def test_create_role_with_invalid_permissions_raises_error(self, db, team_store, test_user):
        """Test creating a role with invalid permission IDs raises ValueError."""
        with pytest.raises(InvalidRoleException, match="Invalid permission IDs"):
            store_team_service.create_custom_role(
                db=db,
                store_id=team_store.id,
                name="invalid_role",
                permissions=["completely.fake.permission"],
                actor_user_id=test_user.id,
            )

    def test_create_role_with_empty_permissions(self, db, team_store, test_user):
        """Test creating a role with empty permissions list."""
        role = store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="observer",
            permissions=[],
            actor_user_id=test_user.id,
        )
        db.commit()

        assert role is not None
        assert role.name == "observer"
        assert role.permissions == []


@pytest.mark.unit
@pytest.mark.tenancy
class TestStoreTeamServiceUpdateCustomRole:
    """Test suite for updating roles."""

    def test_update_role_name(self, db, team_store, test_user):
        """Test updating a role's name."""
        role = store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="old_name",
            permissions=["products.view"],
            actor_user_id=test_user.id,
        )
        db.flush()

        updated = store_team_service.update_role(
            db=db,
            store_id=team_store.id,
            role_id=role.id,
            name="new_name",
            actor_user_id=test_user.id,
        )
        db.commit()

        assert updated.name == "new_name"

    def test_update_role_permissions(self, db, team_store, test_user):
        """Test updating a role's permissions."""
        role = store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="perm_test",
            permissions=["products.view"],
            actor_user_id=test_user.id,
        )
        db.flush()

        updated = store_team_service.update_role(
            db=db,
            store_id=team_store.id,
            role_id=role.id,
            permissions=["products.view", "products.edit", "orders.view"],
            actor_user_id=test_user.id,
        )
        db.commit()

        assert "products.edit" in updated.permissions
        assert "orders.view" in updated.permissions

    def test_update_role_not_found_raises_error(self, db, team_store):
        """Test updating a non-existent role raises ValueError."""
        with pytest.raises(InvalidRoleException, match="not found"):
            store_team_service.update_role(
                db=db,
                store_id=team_store.id,
                role_id=99999,
                name="new_name",
            )

    def test_update_role_rename_to_preset_raises_error(self, db, team_store, test_user):
        """Test renaming to a preset name raises ValueError."""
        role = store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="custom_role",
            permissions=["products.view"],
            actor_user_id=test_user.id,
        )
        db.flush()

        with pytest.raises(InvalidRoleException, match="preset name"):
            store_team_service.update_role(
                db=db,
                store_id=team_store.id,
                role_id=role.id,
                name="staff",
            )

    def test_update_role_with_invalid_permissions(self, db, team_store, test_user):
        """Test updating with invalid permissions raises ValueError."""
        role = store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="to_update",
            permissions=["products.view"],
            actor_user_id=test_user.id,
        )
        db.flush()

        with pytest.raises(InvalidRoleException, match="Invalid permission IDs"):
            store_team_service.update_role(
                db=db,
                store_id=team_store.id,
                role_id=role.id,
                permissions=["nonexistent.perm"],
            )

    def test_update_role_duplicate_name_raises_error(self, db, team_store, test_user):
        """Test renaming to an existing role name raises ValueError."""
        store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="role_a",
            permissions=["products.view"],
            actor_user_id=test_user.id,
        )
        role_b = store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="role_b",
            permissions=["orders.view"],
            actor_user_id=test_user.id,
        )
        db.flush()

        with pytest.raises(InvalidRoleException, match="already exists"):
            store_team_service.update_role(
                db=db,
                store_id=team_store.id,
                role_id=role_b.id,
                name="role_a",
            )


@pytest.mark.unit
@pytest.mark.tenancy
class TestStoreTeamServiceDeleteRole:
    """Test suite for deleting roles."""

    def test_delete_custom_role_success(self, db, team_store, test_user):
        """Test deleting a custom role."""
        role = store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="to_delete",
            permissions=["products.view"],
            actor_user_id=test_user.id,
        )
        db.flush()
        role_id = role.id

        store_team_service.delete_role(
            db=db,
            store_id=team_store.id,
            role_id=role_id,
            actor_user_id=test_user.id,
        )
        db.commit()

        # Verify role is deleted
        deleted = db.query(Role).filter(Role.id == role_id).first()
        assert deleted is None

    def test_delete_preset_role_raises_error(self, db, team_store):
        """Test deleting a preset role raises ValueError."""
        # Ensure default roles exist
        store_team_service.get_store_roles(db, team_store.id)
        db.flush()

        # Find the "staff" preset role
        staff_role = (
            db.query(Role)
            .filter(Role.store_id == team_store.id, Role.name == "staff")
            .first()
        )
        assert staff_role is not None

        with pytest.raises(InvalidRoleException, match="preset role"):
            store_team_service.delete_role(
                db=db,
                store_id=team_store.id,
                role_id=staff_role.id,
            )

    def test_delete_role_not_found_raises_error(self, db, team_store):
        """Test deleting non-existent role raises ValueError."""
        with pytest.raises(InvalidRoleException, match="not found"):
            store_team_service.delete_role(
                db=db,
                store_id=team_store.id,
                role_id=99999,
            )

    def test_delete_role_with_members_raises_error(self, db, team_store, test_user, other_user, auth_manager):
        """Test deleting a role that has assigned members raises ValueError."""
        role = store_team_service.create_custom_role(
            db=db,
            store_id=team_store.id,
            name="in_use_role",
            permissions=["products.view"],
            actor_user_id=test_user.id,
        )
        db.flush()

        # Assign the role to a store user
        store_user = StoreUser(
            store_id=team_store.id,
            user_id=other_user.id,
            role_id=role.id,
            is_active=True,
            invitation_accepted_at=datetime.utcnow(),
        )
        db.add(store_user)
        db.flush()

        with pytest.raises(InvalidRoleException, match="team member"):
            store_team_service.delete_role(
                db=db,
                store_id=team_store.id,
                role_id=role.id,
            )


# =============================================================================
# ADMIN STORE ACCESS VALIDATION
# =============================================================================


@pytest.mark.unit
@pytest.mark.tenancy
class TestValidateAdminStoreAccess:
    """Tests for validate_admin_store_access()."""

    def test_super_admin_can_access_any_store(self, db, team_store):
        """Super admin (accessible_platform_ids=None) can access any store."""
        from unittest.mock import MagicMock

        user_ctx = MagicMock()
        user_ctx.get_accessible_platform_ids.return_value = None

        store = store_team_service.validate_admin_store_access(
            db, user_ctx, team_store.id
        )
        assert store.id == team_store.id

    def test_platform_admin_can_access_store_in_their_platform(self, db, team_store):
        """Platform admin can access stores in their assigned platform."""
        from unittest.mock import MagicMock

        # Create a platform and link the store
        platform = Platform(
            code=f"test_plat_{uuid.uuid4().hex[:6]}",
            name="Test Platform",
            is_active=True,
        )
        db.add(platform)
        db.flush()

        sp = StorePlatform(
            store_id=team_store.id,
            platform_id=platform.id,
            is_active=True,
        )
        db.add(sp)
        db.flush()

        user_ctx = MagicMock()
        user_ctx.get_accessible_platform_ids.return_value = [platform.id]

        store = store_team_service.validate_admin_store_access(
            db, user_ctx, team_store.id
        )
        assert store.id == team_store.id

    def test_platform_admin_cannot_access_store_outside_platform(self, db, team_store):
        """Platform admin cannot access stores outside their platform."""
        from unittest.mock import MagicMock

        user_ctx = MagicMock()
        # Platform ID 99999 does not have the test store
        user_ctx.get_accessible_platform_ids.return_value = [99999]

        with pytest.raises(InvalidRoleException, match="do not have access"):
            store_team_service.validate_admin_store_access(
                db, user_ctx, team_store.id
            )

    def test_nonexistent_store_raises_error(self, db):
        """Accessing a nonexistent store raises InvalidRoleException."""
        from unittest.mock import MagicMock

        user_ctx = MagicMock()
        user_ctx.get_accessible_platform_ids.return_value = None

        with pytest.raises(InvalidRoleException, match="not found"):
            store_team_service.validate_admin_store_access(
                db, user_ctx, 99999
            )
