# tests/api/test_organizations.py
"""
Tests for organization routes (user endpoints).

These test the routes in app/api/routes/organizations.py which allow
users to view and manage organizations they belong to.
"""

from unittest.mock import patch
from uuid import uuid4

import pytest
import pytest_asyncio
from fastapi import status

from app.core.auth import get_password_hash
from app.models.organization import Organization
from app.models.user import User
from app.models.user_organization import OrganizationRole, UserOrganization


@pytest_asyncio.fixture
async def user_token(client, async_test_user):
    """Get access token for regular user."""
    response = await client.post(
        "/api/v1/auth/login",
        json={"email": "testuser@example.com", "password": "TestPassword123!"},
    )
    assert response.status_code == 200
    return response.json()["access_token"]


@pytest_asyncio.fixture
async def second_user(async_test_db):
    """Create a second test user."""
    _test_engine, AsyncTestingSessionLocal = async_test_db
    async with AsyncTestingSessionLocal() as session:
        user = User(
            id=uuid4(),
            email="seconduser@example.com",
            password_hash=get_password_hash("TestPassword123!"),
            first_name="Second",
            last_name="User",
            phone_number="+1234567891",
            is_active=True,
            is_superuser=False,
            preferences=None,
        )
        session.add(user)
        await session.commit()
        await session.refresh(user)
        return user


@pytest_asyncio.fixture
async def test_org_with_user_member(async_test_db, async_test_user):
    """Create a test organization with async_test_user as a member."""
    _test_engine, AsyncTestingSessionLocal = async_test_db
    async with AsyncTestingSessionLocal() as session:
        org = Organization(
            name="Member Org",
            slug="member-org",
            description="Test organization where user is a member",
        )
        session.add(org)
        await session.commit()
        await session.refresh(org)

        # Add user as member
        membership = UserOrganization(
            user_id=async_test_user.id,
            organization_id=org.id,
            role=OrganizationRole.MEMBER,
            is_active=True,
        )
        session.add(membership)
        await session.commit()

        return org


@pytest_asyncio.fixture
async def test_org_with_user_admin(async_test_db, async_test_user):
    """Create a test organization with async_test_user as an admin."""
    _test_engine, AsyncTestingSessionLocal = async_test_db
    async with AsyncTestingSessionLocal() as session:
        org = Organization(
            name="Admin Org",
            slug="admin-org",
            description="Test organization where user is an admin",
        )
        session.add(org)
        await session.commit()
        await session.refresh(org)

        # Add user as admin
        membership = UserOrganization(
            user_id=async_test_user.id,
            organization_id=org.id,
            role=OrganizationRole.ADMIN,
            is_active=True,
        )
        session.add(membership)
        await session.commit()

        return org


@pytest_asyncio.fixture
async def test_org_with_user_owner(async_test_db, async_test_user):
    """Create a test organization with async_test_user as owner."""
    _test_engine, AsyncTestingSessionLocal = async_test_db
    async with AsyncTestingSessionLocal() as session:
        org = Organization(
            name="Owner Org",
            slug="owner-org",
            description="Test organization where user is owner",
        )
        session.add(org)
        await session.commit()
        await session.refresh(org)

        # Add user as owner
        membership = UserOrganization(
            user_id=async_test_user.id,
            organization_id=org.id,
            role=OrganizationRole.OWNER,
            is_active=True,
        )
        session.add(membership)
        await session.commit()

        return org


# ===== GET /api/v1/organizations/me =====


class TestGetMyOrganizations:
    """Tests for GET /api/v1/organizations/me endpoint."""

    @pytest.mark.asyncio
    async def test_get_my_organizations_success(
        self, client, user_token, test_org_with_user_member, test_org_with_user_admin
    ):
        """Test successfully getting user's organizations (covers lines 54-79)."""
        response = await client.get(
            "/api/v1/organizations/me",
            headers={"Authorization": f"Bearer {user_token}"},
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert isinstance(data, list)
        assert len(data) >= 2  # At least the two test orgs

        # Verify structure
        for org in data:
            assert "id" in org
            assert "name" in org
            assert "slug" in org
            assert "member_count" in org

    @pytest.mark.asyncio
    async def test_get_my_organizations_filter_active(
        self, client, async_test_db, async_test_user, user_token
    ):
        """Test filtering organizations by active status."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create active org
        async with AsyncTestingSessionLocal() as session:
            active_org = Organization(
                name="Active Org", slug="active-org-filter", is_active=True
            )
            session.add(active_org)
            await session.commit()
            await session.refresh(active_org)

            # Add user membership
            membership = UserOrganization(
                user_id=async_test_user.id,
                organization_id=active_org.id,
                role=OrganizationRole.MEMBER,
                is_active=True,
            )
            session.add(membership)
            await session.commit()

        response = await client.get(
            "/api/v1/organizations/me?is_active=true",
            headers={"Authorization": f"Bearer {user_token}"},
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert isinstance(data, list)

    @pytest.mark.asyncio
    async def test_get_my_organizations_empty(self, client, async_test_db):
        """Test getting organizations when user has none."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create user with no org memberships
        async with AsyncTestingSessionLocal() as session:
            user = User(
                id=uuid4(),
                email="noorg@example.com",
                password_hash=get_password_hash("TestPassword123!"),
                first_name="No",
                last_name="Org",
                is_active=True,
            )
            session.add(user)
            await session.commit()

        # Login to get token
        login_response = await client.post(
            "/api/v1/auth/login",
            json={"email": "noorg@example.com", "password": "TestPassword123!"},
        )
        token = login_response.json()["access_token"]

        response = await client.get(
            "/api/v1/organizations/me", headers={"Authorization": f"Bearer {token}"}
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data == []


# ===== GET /api/v1/organizations/{organization_id} =====


class TestGetOrganization:
    """Tests for GET /api/v1/organizations/{organization_id} endpoint."""

    @pytest.mark.asyncio
    async def test_get_organization_success(
        self, client, user_token, test_org_with_user_member
    ):
        """Test successfully getting organization details (covers lines 103-122)."""
        response = await client.get(
            f"/api/v1/organizations/{test_org_with_user_member.id}",
            headers={"Authorization": f"Bearer {user_token}"},
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["id"] == str(test_org_with_user_member.id)
        assert data["name"] == "Member Org"
        assert data["slug"] == "member-org"
        assert "member_count" in data

    @pytest.mark.asyncio
    async def test_get_organization_not_found(self, client, user_token):
        """Test getting nonexistent organization returns 403 (permission check happens first)."""
        fake_org_id = uuid4()
        response = await client.get(
            f"/api/v1/organizations/{fake_org_id}",
            headers={"Authorization": f"Bearer {user_token}"},
        )

        # Permission dependency checks membership before endpoint logic
        # So non-existent org returns 403 (not a member) instead of 404
        assert response.status_code == status.HTTP_403_FORBIDDEN
        data = response.json()
        assert "errors" in data or "detail" in data

    @pytest.mark.asyncio
    async def test_get_organization_not_member(
        self, client, async_test_db, async_test_user
    ):
        """Test getting organization where user is not a member fails."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create org without adding user
        async with AsyncTestingSessionLocal() as session:
            org = Organization(name="Not Member Org", slug="not-member-org")
            session.add(org)
            await session.commit()
            await session.refresh(org)
            org_id = org.id

        # Login as user
        login_response = await client.post(
            "/api/v1/auth/login",
            json={"email": "testuser@example.com", "password": "TestPassword123!"},
        )
        token = login_response.json()["access_token"]

        response = await client.get(
            f"/api/v1/organizations/{org_id}",
            headers={"Authorization": f"Bearer {token}"},
        )

        # Should fail permission check
        assert response.status_code == status.HTTP_403_FORBIDDEN


# ===== GET /api/v1/organizations/{organization_id}/members =====


class TestGetOrganizationMembers:
    """Tests for GET /api/v1/organizations/{organization_id}/members endpoint."""

    @pytest.mark.asyncio
    async def test_get_organization_members_success(
        self,
        client,
        async_test_db,
        async_test_user,
        second_user,
        user_token,
        test_org_with_user_member,
    ):
        """Test successfully getting organization members (covers lines 150-168)."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Add second user to org
        async with AsyncTestingSessionLocal() as session:
            membership = UserOrganization(
                user_id=second_user.id,
                organization_id=test_org_with_user_member.id,
                role=OrganizationRole.MEMBER,
                is_active=True,
            )
            session.add(membership)
            await session.commit()

        response = await client.get(
            f"/api/v1/organizations/{test_org_with_user_member.id}/members",
            headers={"Authorization": f"Bearer {user_token}"},
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert "data" in data
        assert "pagination" in data
        assert len(data["data"]) >= 2  # At least the two users

    @pytest.mark.asyncio
    async def test_get_organization_members_with_pagination(
        self, client, user_token, test_org_with_user_member
    ):
        """Test pagination parameters."""
        response = await client.get(
            f"/api/v1/organizations/{test_org_with_user_member.id}/members?page=1&limit=10",
            headers={"Authorization": f"Bearer {user_token}"},
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["pagination"]["page"] == 1
        assert "page_size" in data["pagination"]  # Uses page_size, not limit
        assert "total" in data["pagination"]

    @pytest.mark.asyncio
    async def test_get_organization_members_filter_active(
        self,
        client,
        async_test_db,
        async_test_user,
        second_user,
        user_token,
        test_org_with_user_member,
    ):
        """Test filtering members by active status."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Add second user as inactive member
        async with AsyncTestingSessionLocal() as session:
            membership = UserOrganization(
                user_id=second_user.id,
                organization_id=test_org_with_user_member.id,
                role=OrganizationRole.MEMBER,
                is_active=False,
            )
            session.add(membership)
            await session.commit()

        # Filter for active only
        response = await client.get(
            f"/api/v1/organizations/{test_org_with_user_member.id}/members?is_active=true",
            headers={"Authorization": f"Bearer {user_token}"},
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        # Should only see active members
        for member in data["data"]:
            assert member["is_active"] is True


# ===== PUT /api/v1/organizations/{organization_id} =====


class TestUpdateOrganization:
    """Tests for PUT /api/v1/organizations/{organization_id} endpoint."""

    @pytest.mark.asyncio
    async def test_update_organization_as_admin_success(
        self, client, async_test_user, test_org_with_user_admin
    ):
        """Test successfully updating organization as admin (covers lines 193-215)."""
        # Login as admin user
        login_response = await client.post(
            "/api/v1/auth/login",
            json={"email": "testuser@example.com", "password": "TestPassword123!"},
        )
        admin_token = login_response.json()["access_token"]

        response = await client.put(
            f"/api/v1/organizations/{test_org_with_user_admin.id}",
            json={"name": "Updated Admin Org", "description": "Updated description"},
            headers={"Authorization": f"Bearer {admin_token}"},
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["name"] == "Updated Admin Org"
        assert data["description"] == "Updated description"

    @pytest.mark.asyncio
    async def test_update_organization_as_owner_success(
        self, client, async_test_user, test_org_with_user_owner
    ):
        """Test successfully updating organization as owner."""
        # Login as owner user
        login_response = await client.post(
            "/api/v1/auth/login",
            json={"email": "testuser@example.com", "password": "TestPassword123!"},
        )
        owner_token = login_response.json()["access_token"]

        response = await client.put(
            f"/api/v1/organizations/{test_org_with_user_owner.id}",
            json={"name": "Updated Owner Org"},
            headers={"Authorization": f"Bearer {owner_token}"},
        )

        assert response.status_code == status.HTTP_200_OK
        data = response.json()
        assert data["name"] == "Updated Owner Org"

    @pytest.mark.asyncio
    async def test_update_organization_as_member_fails(
        self, client, user_token, test_org_with_user_member
    ):
        """Test updating organization as regular member fails."""
        response = await client.put(
            f"/api/v1/organizations/{test_org_with_user_member.id}",
            json={"name": "Should Fail"},
            headers={"Authorization": f"Bearer {user_token}"},
        )

        # Should fail permission check (need admin or owner)
        assert response.status_code == status.HTTP_403_FORBIDDEN

    @pytest.mark.asyncio
    async def test_update_organization_not_found(
        self, client, test_org_with_user_admin
    ):
        """Test updating nonexistent organization returns 403 (permission check first)."""
        # Login as admin
        login_response = await client.post(
            "/api/v1/auth/login",
            json={"email": "testuser@example.com", "password": "TestPassword123!"},
        )
        admin_token = login_response.json()["access_token"]

        fake_org_id = uuid4()
        response = await client.put(
            f"/api/v1/organizations/{fake_org_id}",
            json={"name": "Updated"},
            headers={"Authorization": f"Bearer {admin_token}"},
        )

        # Permission dependency checks admin role before endpoint logic
        # So non-existent org returns 403 (not an admin) instead of 404
        assert response.status_code == status.HTTP_403_FORBIDDEN
        data = response.json()
        assert "errors" in data or "detail" in data


# ===== Authentication Tests =====


class TestOrganizationAuthentication:
    """Test authentication requirements for organization endpoints."""

    @pytest.mark.asyncio
    async def test_get_my_organizations_unauthenticated(self, client):
        """Test unauthenticated access to /me fails."""
        response = await client.get("/api/v1/organizations/me")
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    @pytest.mark.asyncio
    async def test_get_organization_unauthenticated(self, client):
        """Test unauthenticated access to organization details fails."""
        fake_id = uuid4()
        response = await client.get(f"/api/v1/organizations/{fake_id}")
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    @pytest.mark.asyncio
    async def test_get_members_unauthenticated(self, client):
        """Test unauthenticated access to members list fails."""
        fake_id = uuid4()
        response = await client.get(f"/api/v1/organizations/{fake_id}/members")
        assert response.status_code == status.HTTP_401_UNAUTHORIZED

    @pytest.mark.asyncio
    async def test_update_organization_unauthenticated(self, client):
        """Test unauthenticated access to update fails."""
        fake_id = uuid4()
        response = await client.put(
            f"/api/v1/organizations/{fake_id}", json={"name": "Test"}
        )
        assert response.status_code == status.HTTP_401_UNAUTHORIZED


# ===== Exception Handler Tests (Database Error Scenarios) =====


class TestOrganizationExceptionHandlers:
    """
    Test exception handlers in organization endpoints.

    These tests use mocks to trigger database errors and verify
    proper error handling. Covers lines: 81-83, 124-128, 170-172, 217-221
    """

    @pytest.mark.asyncio
    async def test_get_my_organizations_database_error(
        self, client, user_token, test_org_with_user_member
    ):
        """Test generic exception handler in get_my_organizations (covers lines 81-83)."""
        with patch(
            "app.crud.organization.organization.get_user_organizations_with_details",
            side_effect=Exception("Database connection lost"),
        ):
            # The exception handler logs and re-raises, so we expect the exception
            # to propagate (which proves the handler executed)
            with pytest.raises(Exception, match="Database connection lost"):
                await client.get(
                    "/api/v1/organizations/me",
                    headers={"Authorization": f"Bearer {user_token}"},
                )

    @pytest.mark.asyncio
    async def test_get_organization_database_error(
        self, client, user_token, test_org_with_user_member
    ):
        """Test generic exception handler in get_organization (covers lines 124-128)."""
        with patch(
            "app.crud.organization.organization.get",
            side_effect=Exception("Database timeout"),
        ):
            with pytest.raises(Exception, match="Database timeout"):
                await client.get(
                    f"/api/v1/organizations/{test_org_with_user_member.id}",
                    headers={"Authorization": f"Bearer {user_token}"},
                )

    @pytest.mark.asyncio
    async def test_get_organization_members_database_error(
        self, client, user_token, test_org_with_user_member
    ):
        """Test generic exception handler in get_organization_members (covers lines 170-172)."""
        with patch(
            "app.crud.organization.organization.get_organization_members",
            side_effect=Exception("Connection pool exhausted"),
        ):
            with pytest.raises(Exception, match="Connection pool exhausted"):
                await client.get(
                    f"/api/v1/organizations/{test_org_with_user_member.id}/members",
                    headers={"Authorization": f"Bearer {user_token}"},
                )

    @pytest.mark.asyncio
    async def test_update_organization_database_error(
        self, client, async_test_user, test_org_with_user_admin
    ):
        """Test generic exception handler in update_organization (covers lines 217-221)."""
        # Login as admin user
        login_response = await client.post(
            "/api/v1/auth/login",
            json={"email": "testuser@example.com", "password": "TestPassword123!"},
        )
        admin_token = login_response.json()["access_token"]

        with patch(
            "app.crud.organization.organization.get",
            return_value=test_org_with_user_admin,
        ):
            with patch(
                "app.crud.organization.organization.update",
                side_effect=Exception("Write lock timeout"),
            ):
                with pytest.raises(Exception, match="Write lock timeout"):
                    await client.put(
                        f"/api/v1/organizations/{test_org_with_user_admin.id}",
                        json={"name": "Should Fail"},
                        headers={"Authorization": f"Bearer {admin_token}"},
                    )
