from __future__ import annotations

import logging
from uuid import uuid4

import pytest
from sqlalchemy.orm import Session

from models import TenantAccountRole
from models.account import Account, Tenant, TenantAccountJoin
from models.enums import EndUserType
from models.model import App, DefaultEndUserSessionID, EndUser
from services.end_user_service import EndUserService


class TestEndUserServiceFactory:
    """Factory class for creating test data and mock objects for end user service tests."""

    @staticmethod
    def create_app_and_account(db_session_with_containers: Session):
        tenant = Tenant(name=f"Tenant {uuid4()}")
        db_session_with_containers.add(tenant)
        db_session_with_containers.flush()

        account = Account(
            name=f"Account {uuid4()}",
            email=f"end_user_{uuid4()}@example.com",
            password="hashed-password",
            password_salt="salt",
            interface_language="en-US",
            timezone="UTC",
        )
        db_session_with_containers.add(account)
        db_session_with_containers.flush()

        tenant_join = TenantAccountJoin(
            tenant_id=tenant.id,
            account_id=account.id,
            role=TenantAccountRole.OWNER,
            current=True,
        )
        db_session_with_containers.add(tenant_join)
        db_session_with_containers.flush()

        app = App(
            tenant_id=tenant.id,
            name=f"App {uuid4()}",
            description="",
            mode="chat",
            icon_type="emoji",
            icon="bot",
            icon_background="#FFFFFF",
            enable_site=False,
            enable_api=True,
            api_rpm=100,
            api_rph=100,
            is_demo=False,
            is_public=False,
            is_universal=False,
            created_by=account.id,
            updated_by=account.id,
        )
        db_session_with_containers.add(app)
        db_session_with_containers.commit()
        return app

    @staticmethod
    def create_end_user(
        db_session_with_containers,
        *,
        tenant_id: str,
        app_id: str,
        session_id: str,
        invoke_type: EndUserType,
        is_anonymous: bool = False,
    ):
        end_user = EndUser(
            tenant_id=tenant_id,
            app_id=app_id,
            type=invoke_type,
            external_user_id=session_id,
            name=f"User-{uuid4()}",
            is_anonymous=is_anonymous,
            session_id=session_id,
        )
        db_session_with_containers.add(end_user)
        db_session_with_containers.commit()
        return end_user


class TestEndUserServiceGetOrCreateEndUser:
    """
    Unit tests for EndUserService.get_or_create_end_user method.

    This test suite covers:
    - Creating new end users
    - Retrieving existing end users
    - Default session ID handling
    - Anonymous user creation
    """

    @pytest.fixture
    def factory(self):
        """Provide test data factory."""
        return TestEndUserServiceFactory()

    def test_get_or_create_end_user_with_custom_user_id(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        """Test getting or creating end user with custom user_id."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        user_id = "custom-user-123"

        # Act
        result = EndUserService.get_or_create_end_user(app_model=app, user_id=user_id)

        # Assert
        assert result.tenant_id == app.tenant_id
        assert result.app_id == app.id
        assert result.session_id == user_id
        assert result.type == EndUserType.SERVICE_API
        assert result.is_anonymous is False

    def test_get_or_create_end_user_without_user_id(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        """Test getting or creating end user without user_id uses default session."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)

        # Act
        result = EndUserService.get_or_create_end_user(app_model=app, user_id=None)

        # Assert
        assert result.session_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID
        # Verify _is_anonymous is set correctly (property always returns False)
        assert result._is_anonymous is True

    def test_get_existing_end_user(self, db_session_with_containers: Session, factory: TestEndUserServiceFactory):
        """Test retrieving an existing end user."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        user_id = "existing-user-123"
        existing_user = factory.create_end_user(
            db_session_with_containers,
            tenant_id=app.tenant_id,
            app_id=app.id,
            session_id=user_id,
            invoke_type=EndUserType.SERVICE_API,
        )

        # Act
        result = EndUserService.get_or_create_end_user(app_model=app, user_id=user_id)

        # Assert
        assert result.id == existing_user.id


class TestEndUserServiceGetOrCreateEndUserByType:
    """
    Unit tests for EndUserService.get_or_create_end_user_by_type method.

    This test suite covers:
    - Creating end users with different EndUserType values
    - Type migration for legacy users
    - Query ordering and prioritization
    - Session management
    """

    @pytest.fixture
    def factory(self):
        """Provide test data factory."""
        return TestEndUserServiceFactory()

    def test_create_end_user_service_api_type(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        """Test creating new end user with SERVICE_API type."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        tenant_id = app.tenant_id
        app_id = app.id
        user_id = "user-789"

        # Act
        result = EndUserService.get_or_create_end_user_by_type(
            type=EndUserType.SERVICE_API,
            tenant_id=tenant_id,
            app_id=app_id,
            user_id=user_id,
        )

        # Assert
        assert result.type == EndUserType.SERVICE_API
        assert result.tenant_id == tenant_id
        assert result.app_id == app_id
        assert result.session_id == user_id

    def test_create_end_user_browser_type(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        """Test creating new end user with BROWSER type."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        tenant_id = app.tenant_id
        app_id = app.id
        user_id = "user-789"

        # Act
        result = EndUserService.get_or_create_end_user_by_type(
            type=EndUserType.BROWSER,
            tenant_id=tenant_id,
            app_id=app_id,
            user_id=user_id,
        )

        # Assert
        assert result.type == EndUserType.BROWSER

    def test_upgrade_legacy_end_user_type(
        self, caplog: pytest.LogCaptureFixture, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        """Test upgrading legacy end user with different type."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        tenant_id = app.tenant_id
        app_id = app.id
        user_id = "user-789"

        # Existing user with old type
        existing_user = factory.create_end_user(
            db_session_with_containers,
            tenant_id=tenant_id,
            app_id=app_id,
            session_id=user_id,
            invoke_type=EndUserType.SERVICE_API,
        )
        with caplog.at_level(logging.INFO, logger="services.end_user_service"):
            # Act - Request with different type
            result = EndUserService.get_or_create_end_user_by_type(
                type=EndUserType.BROWSER,
                tenant_id=tenant_id,
                app_id=app_id,
                user_id=user_id,
            )

        # Assert
        assert result.id == existing_user.id
        assert result.type == EndUserType.BROWSER  # Type should be updated
        matching_logs = [
            record
            for record in caplog.records
            if record.name == "services.end_user_service"
            and record.levelno == logging.INFO
            and "Upgrading legacy EndUser" in record.message
        ]

        assert len(matching_logs) == 1

    def test_get_existing_end_user_matching_type(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory, caplog
    ):
        """Test retrieving existing end user with matching type."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        tenant_id = app.tenant_id
        app_id = app.id
        user_id = "user-789"

        existing_user = factory.create_end_user(
            db_session_with_containers,
            tenant_id=tenant_id,
            app_id=app_id,
            session_id=user_id,
            invoke_type=EndUserType.SERVICE_API,
        )

        # Act - Request with same type
        with caplog.at_level(logging.INFO, logger="services.end_user_service"):
            result = EndUserService.get_or_create_end_user_by_type(
                type=EndUserType.SERVICE_API,
                tenant_id=tenant_id,
                app_id=app_id,
                user_id=user_id,
            )

        # Assert
        assert result.id == existing_user.id
        assert result.type == EndUserType.SERVICE_API
        # No legacy-upgrade log should be emitted when the existing user's type already matches.
        assert [record for record in caplog.records if record.levelno == logging.INFO] == []

    def test_create_anonymous_user_with_default_session(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        """Test creating anonymous user when user_id is None."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        tenant_id = app.tenant_id
        app_id = app.id

        # Act
        result = EndUserService.get_or_create_end_user_by_type(
            type=EndUserType.SERVICE_API,
            tenant_id=tenant_id,
            app_id=app_id,
            user_id=None,
        )

        # Assert
        assert result.session_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID
        # Verify _is_anonymous is set correctly (property always returns False)
        assert result._is_anonymous is True
        assert result.external_user_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID

    def test_query_ordering_prioritizes_matching_type(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        """Test that query ordering prioritizes records with matching type."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        tenant_id = app.tenant_id
        app_id = app.id
        user_id = "user-789"

        non_matching = factory.create_end_user(
            db_session_with_containers,
            tenant_id=tenant_id,
            app_id=app_id,
            session_id=user_id,
            invoke_type=EndUserType.BROWSER,
        )
        matching = factory.create_end_user(
            db_session_with_containers,
            tenant_id=tenant_id,
            app_id=app_id,
            session_id=user_id,
            invoke_type=EndUserType.SERVICE_API,
        )

        # Act
        result = EndUserService.get_or_create_end_user_by_type(
            type=EndUserType.SERVICE_API,
            tenant_id=tenant_id,
            app_id=app_id,
            user_id=user_id,
        )

        # Assert
        assert result.id == matching.id
        assert result.id != non_matching.id

    def test_external_user_id_matches_session_id(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        """Test that external_user_id is set to match session_id."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        tenant_id = app.tenant_id
        app_id = app.id
        user_id = "custom-external-id"

        # Act
        result = EndUserService.get_or_create_end_user_by_type(
            type=EndUserType.SERVICE_API,
            tenant_id=tenant_id,
            app_id=app_id,
            user_id=user_id,
        )

        # Assert
        assert result.external_user_id == user_id
        assert result.session_id == user_id

    @pytest.mark.parametrize(
        "invoke_type",
        [
            EndUserType.SERVICE_API,
            EndUserType.BROWSER,
            EndUserType.OPENAPI,
            EndUserType.TRIGGER,
        ],
    )
    def test_create_end_user_with_different_invoke_types(
        self, db_session_with_containers: Session, invoke_type: EndUserType, factory: TestEndUserServiceFactory
    ):
        """Test creating end users with different EndUserType values."""
        # Arrange
        app = factory.create_app_and_account(db_session_with_containers)
        tenant_id = app.tenant_id
        app_id = app.id
        user_id = f"user-{uuid4()}"

        # Act
        result = EndUserService.get_or_create_end_user_by_type(
            type=invoke_type,
            tenant_id=tenant_id,
            app_id=app_id,
            user_id=user_id,
        )

        # Assert
        assert result.type == invoke_type


class TestEndUserServiceGetEndUserById:
    """Unit tests for EndUserService.get_end_user_by_id."""

    @pytest.fixture
    def factory(self):
        """Provide test data factory."""
        return TestEndUserServiceFactory()

    def test_get_end_user_by_id_returns_end_user(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        app = factory.create_app_and_account(db_session_with_containers)
        existing_user = factory.create_end_user(
            db_session_with_containers,
            tenant_id=app.tenant_id,
            app_id=app.id,
            session_id=f"session-{uuid4()}",
            invoke_type=EndUserType.SERVICE_API,
        )

        result = EndUserService.get_end_user_by_id(
            tenant_id=app.tenant_id,
            app_id=app.id,
            end_user_id=existing_user.id,
        )

        assert result is not None
        assert result.id == existing_user.id

    def test_get_end_user_by_id_returns_none(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        app = factory.create_app_and_account(db_session_with_containers)

        result = EndUserService.get_end_user_by_id(
            tenant_id=app.tenant_id,
            app_id=app.id,
            end_user_id=str(uuid4()),
        )

        assert result is None


class TestEndUserServiceCreateBatch:
    """Integration tests for EndUserService.create_end_user_batch."""

    @pytest.fixture
    def factory(self):
        return TestEndUserServiceFactory()

    def _create_multiple_apps(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory, count: int = 3
    ):
        """Create multiple apps under the same tenant."""
        first_app = factory.create_app_and_account(db_session_with_containers)
        tenant_id = first_app.tenant_id
        apps = [first_app]
        for _ in range(count - 1):
            app = App(
                tenant_id=tenant_id,
                name=f"App {uuid4()}",
                description="",
                mode="chat",
                icon_type="emoji",
                icon="bot",
                icon_background="#FFFFFF",
                enable_site=False,
                enable_api=True,
                api_rpm=100,
                api_rph=100,
                is_demo=False,
                is_public=False,
                is_universal=False,
                created_by=first_app.created_by,
                updated_by=first_app.updated_by,
            )
            db_session_with_containers.add(app)
        db_session_with_containers.commit()
        all_apps = db_session_with_containers.query(App).filter(App.tenant_id == tenant_id).all()
        return tenant_id, all_apps

    def test_create_batch_empty_app_ids(self, db_session_with_containers: Session):
        result = EndUserService.create_end_user_batch(
            type=EndUserType.SERVICE_API, tenant_id=str(uuid4()), app_ids=[], user_id="user-1"
        )
        assert result == {}

    def test_create_batch_creates_users_for_all_apps(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        tenant_id, apps = self._create_multiple_apps(db_session_with_containers, factory, count=3)
        app_ids = [a.id for a in apps]
        user_id = f"user-{uuid4()}"

        result = EndUserService.create_end_user_batch(
            type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
        )

        assert len(result) == 3
        for app_id in app_ids:
            assert app_id in result
            assert result[app_id].session_id == user_id
            assert result[app_id].type == EndUserType.SERVICE_API

    def test_create_batch_default_session_id(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        tenant_id, apps = self._create_multiple_apps(db_session_with_containers, factory, count=2)
        app_ids = [a.id for a in apps]

        result = EndUserService.create_end_user_batch(
            type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=""
        )

        assert len(result) == 2
        for end_user in result.values():
            assert end_user.session_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID
            assert end_user._is_anonymous is True

    def test_create_batch_deduplicate_app_ids(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        tenant_id, apps = self._create_multiple_apps(db_session_with_containers, factory, count=2)
        app_ids = [apps[0].id, apps[1].id, apps[0].id, apps[1].id]
        user_id = f"user-{uuid4()}"

        result = EndUserService.create_end_user_batch(
            type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
        )

        assert len(result) == 2

    def test_create_batch_returns_existing_users(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        tenant_id, apps = self._create_multiple_apps(db_session_with_containers, factory, count=2)
        app_ids = [a.id for a in apps]
        user_id = f"user-{uuid4()}"

        # Create batch first time
        first_result = EndUserService.create_end_user_batch(
            type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
        )

        # Create batch second time — should return existing users
        second_result = EndUserService.create_end_user_batch(
            type=EndUserType.SERVICE_API, tenant_id=tenant_id, app_ids=app_ids, user_id=user_id
        )

        assert len(second_result) == 2
        for app_id in app_ids:
            assert first_result[app_id].id == second_result[app_id].id

    def test_create_batch_partial_existing_users(
        self, db_session_with_containers: Session, factory: TestEndUserServiceFactory
    ):
        tenant_id, apps = self._create_multiple_apps(db_session_with_containers, factory, count=3)
        user_id = f"user-{uuid4()}"

        # Create for first 2 apps
        first_result = EndUserService.create_end_user_batch(
            type=EndUserType.SERVICE_API,
            tenant_id=tenant_id,
            app_ids=[apps[0].id, apps[1].id],
            user_id=user_id,
        )

        # Create for all 3 apps — should reuse first 2, create 3rd
        all_result = EndUserService.create_end_user_batch(
            type=EndUserType.SERVICE_API,
            tenant_id=tenant_id,
            app_ids=[a.id for a in apps],
            user_id=user_id,
        )

        assert len(all_result) == 3
        assert all_result[apps[0].id].id == first_result[apps[0].id].id
        assert all_result[apps[1].id].id == first_result[apps[1].id].id
        assert all_result[apps[2].id].session_id == user_id

    @pytest.mark.parametrize(
        "invoke_type",
        [EndUserType.SERVICE_API, EndUserType.BROWSER, EndUserType.OPENAPI, EndUserType.TRIGGER],
    )
    def test_create_batch_all_invoke_types(
        self, db_session_with_containers: Session, invoke_type: EndUserType, factory: TestEndUserServiceFactory
    ):
        tenant_id, apps = self._create_multiple_apps(db_session_with_containers, factory, count=1)
        user_id = f"user-{uuid4()}"

        result = EndUserService.create_end_user_batch(
            type=invoke_type, tenant_id=tenant_id, app_ids=[apps[0].id], user_id=user_id
        )

        assert len(result) == 1
        assert result[apps[0].id].type == invoke_type
