import pytest
import httpx
from typing import List, Dict, Any
from decimal import Decimal
from datetime import datetime, timezone

from app.models import (
    User,
    Group,
    Expense,
    ExpenseSplit,
    SettlementActivity,
    UserRoleEnum,
    SplitTypeEnum,
    ExpenseOverallStatusEnum,
    ExpenseSplitStatusEnum
)
from app.schemas.settlement_activity import SettlementActivityPublic, SettlementActivityCreate
from app.schemas.expense import ExpensePublic, ExpenseSplitPublic
from app.core.config import settings # For API prefix

# Assume db_session, event_loop, client are provided by conftest.py or similar setup
# For this example, I'll define basic user/auth fixtures if not assumed from conftest

@pytest.fixture
async def test_user1_api(db_session, client: httpx.AsyncClient) -> Dict[str, Any]:
    user = User(email="api.user1@example.com", name="API User 1", hashed_password="password1")
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    
    # Simulate token login - in a real setup, you'd call a login endpoint
    # For now, just returning user and headers directly for mock authentication
    # This would typically be handled by a dependency override in tests
    # For simplicity, we'll assume current_active_user dependency correctly resolves to this user
    # when these headers are used (or mock the dependency).
    return {"user": user, "headers": {"Authorization": f"Bearer token-for-{user.id}"}}

@pytest.fixture
async def test_user2_api(db_session, client: httpx.AsyncClient) -> Dict[str, Any]:
    user = User(email="api.user2@example.com", name="API User 2", hashed_password="password2")
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    return {"user": user, "headers": {"Authorization": f"Bearer token-for-{user.id}"}}

@pytest.fixture
async def test_group_user1_owner_api(db_session, test_user1_api: Dict[str, Any]) -> Group:
    user1 = test_user1_api["user"]
    group = Group(name="API Test Group", created_by_id=user1.id)
    db_session.add(group)
    await db_session.flush() # Get group.id

    # Add user1 as owner
    from app.models import UserGroup
    user_group_assoc = UserGroup(user_id=user1.id, group_id=group.id, role=UserRoleEnum.owner)
    db_session.add(user_group_assoc)
    await db_session.commit()
    await db_session.refresh(group)
    return group

@pytest.fixture
async def test_expense_in_group_api(db_session, test_user1_api: Dict[str, Any], test_group_user1_owner_api: Group) -> Expense:
    user1 = test_user1_api["user"]
    expense = Expense(
        description="Group API Expense",
        total_amount=Decimal("50.00"),
        currency="USD",
        group_id=test_group_user1_owner_api.id,
        paid_by_user_id=user1.id,
        created_by_user_id=user1.id,
        split_type=SplitTypeEnum.EQUAL,
        overall_settlement_status=ExpenseOverallStatusEnum.unpaid
    )
    db_session.add(expense)
    await db_session.commit()
    await db_session.refresh(expense)
    return expense

@pytest.fixture
async def test_expense_split_for_user2_api(db_session, test_expense_in_group_api: Expense, test_user1_api: Dict[str, Any], test_user2_api: Dict[str, Any]) -> ExpenseSplit:
    user1 = test_user1_api["user"]
    user2 = test_user2_api["user"]
    
    # Split for User 1 (payer)
    split1 = ExpenseSplit(
        expense_id=test_expense_in_group_api.id,
        user_id=user1.id,
        owed_amount=Decimal("25.00"),
        status=ExpenseSplitStatusEnum.unpaid
    )
    # Split for User 2 (owes)
    split2 = ExpenseSplit(
        expense_id=test_expense_in_group_api.id,
        user_id=user2.id,
        owed_amount=Decimal("25.00"),
        status=ExpenseSplitStatusEnum.unpaid
    )
    db_session.add_all([split1, split2])
    
    # Add user2 to the group as a member for permission checks
    from app.models import UserGroup
    user_group_assoc = UserGroup(user_id=user2.id, group_id=test_expense_in_group_api.group_id, role=UserRoleEnum.member)
    db_session.add(user_group_assoc)
    
    await db_session.commit()
    await db_session.refresh(split1)
    await db_session.refresh(split2)
    return split2 # Return the split that user2 owes


# --- Tests for POST /expense_splits/{expense_split_id}/settle ---

@pytest.mark.asyncio
async def test_settle_expense_split_by_self_success(
    client: httpx.AsyncClient,
    test_user2_api: Dict[str, Any], # User2 will settle their own split
    test_expense_split_for_user2_api: ExpenseSplit,
    db_session: AsyncSession # To verify db changes
):
    user2 = test_user2_api["user"]
    user2_headers = test_user2_api["headers"]
    split_to_settle = test_expense_split_for_user2_api

    payload = SettlementActivityCreate(
        expense_split_id=split_to_settle.id,
        paid_by_user_id=user2.id, # User2 is paying
        amount_paid=split_to_settle.owed_amount # Full payment
    )

    response = await client.post(
        f"{settings.API_V1_STR}/expense_splits/{split_to_settle.id}/settle",
        json=payload.model_dump(mode='json'), # Pydantic v2
        headers=user2_headers
    )

    assert response.status_code == 201
    activity_data = response.json()
    assert activity_data["amount_paid"] == str(split_to_settle.owed_amount) # Compare as string due to JSON
    assert activity_data["paid_by_user_id"] == user2.id
    assert activity_data["expense_split_id"] == split_to_settle.id
    assert "id" in activity_data

    # Verify DB state
    await db_session.refresh(split_to_settle)
    assert split_to_settle.status == ExpenseSplitStatusEnum.paid
    assert split_to_settle.paid_at is not None

    # Verify parent expense status (this requires other splits to be paid too)
    # For a focused test, we might need to ensure the other split (user1's share) is also paid.
    # Or, accept 'partially_paid' if only this one is paid.
    parent_expense_id = split_to_settle.expense_id
    parent_expense = await db_session.get(Expense, parent_expense_id)
    await db_session.refresh(parent_expense, attribute_names=['splits']) # Load splits to check status

    all_splits_paid = all(s.status == ExpenseSplitStatusEnum.paid for s in parent_expense.splits)
    if all_splits_paid:
        assert parent_expense.overall_settlement_status == ExpenseOverallStatusEnum.paid
    else:
        assert parent_expense.overall_settlement_status == ExpenseOverallStatusEnum.partially_paid


@pytest.mark.asyncio
async def test_settle_expense_split_by_group_owner_success(
    client: httpx.AsyncClient,
    test_user1_api: Dict[str, Any], # User1 is group owner
    test_user2_api: Dict[str, Any], # User2 owes the split
    test_expense_split_for_user2_api: ExpenseSplit,
    db_session: AsyncSession
):
    user1_headers = test_user1_api["headers"]
    user_who_owes = test_user2_api["user"]
    split_to_settle = test_expense_split_for_user2_api

    payload = SettlementActivityCreate(
        expense_split_id=split_to_settle.id,
        paid_by_user_id=user_who_owes.id, # User1 (owner) records that User2 has paid
        amount_paid=split_to_settle.owed_amount
    )

    response = await client.post(
        f"{settings.API_V1_STR}/expense_splits/{split_to_settle.id}/settle",
        json=payload.model_dump(mode='json'),
        headers=user1_headers # Authenticated as group owner
    )
    assert response.status_code == 201
    activity_data = response.json()
    assert activity_data["paid_by_user_id"] == user_who_owes.id
    assert activity_data["created_by_user_id"] == test_user1_api["user"].id # Activity created by owner

    await db_session.refresh(split_to_settle)
    assert split_to_settle.status == ExpenseSplitStatusEnum.paid

@pytest.mark.asyncio
async def test_settle_expense_split_path_body_id_mismatch(
    client: httpx.AsyncClient, test_user2_api: Dict[str, Any], test_expense_split_for_user2_api: ExpenseSplit
):
    user2_headers = test_user2_api["headers"]
    split_to_settle = test_expense_split_for_user2_api
    payload = SettlementActivityCreate(
        expense_split_id=split_to_settle.id + 1, # Mismatch
        paid_by_user_id=test_user2_api["user"].id,
        amount_paid=split_to_settle.owed_amount
    )
    response = await client.post(
        f"{settings.API_V1_STR}/expense_splits/{split_to_settle.id}/settle",
        json=payload.model_dump(mode='json'), headers=user2_headers
    )
    assert response.status_code == 400 # As per API endpoint logic

@pytest.mark.asyncio
async def test_settle_expense_split_not_found(
    client: httpx.AsyncClient, test_user2_api: Dict[str, Any]
):
    user2_headers = test_user2_api["headers"]
    payload = SettlementActivityCreate(expense_split_id=9999, paid_by_user_id=test_user2_api["user"].id, amount_paid=Decimal("10.00"))
    response = await client.post(
        f"{settings.API_V1_STR}/expense_splits/9999/settle",
        json=payload.model_dump(mode='json'), headers=user2_headers
    )
    assert response.status_code == 404 # ItemNotFoundError

@pytest.mark.asyncio
async def test_settle_expense_split_insufficient_permissions(
    client: httpx.AsyncClient,
    test_user1_api: Dict[str, Any], # User1 is not group owner for this setup, nor involved in split
    test_user2_api: Dict[str, Any],
    test_expense_split_for_user2_api: ExpenseSplit, # User2 owes this
    db_session: AsyncSession
):
    # Create a new user (user3) who is not involved and not an owner
    user3 = User(email="api.user3@example.com", name="API User 3", hashed_password="password3")
    db_session.add(user3)
    await db_session.commit()
    user3_headers = {"Authorization": f"Bearer token-for-{user3.id}"}


    split_owner = test_user2_api["user"] # User2 owns the split
    split_to_settle = test_expense_split_for_user2_api

    payload = SettlementActivityCreate(
        expense_split_id=split_to_settle.id,
        paid_by_user_id=split_owner.id, # User2 is paying
        amount_paid=split_to_settle.owed_amount
    )
    # User3 (neither payer nor group owner) tries to record User2's payment
    response = await client.post(
        f"{settings.API_V1_STR}/expense_splits/{split_to_settle.id}/settle",
        json=payload.model_dump(mode='json'),
        headers=user3_headers # Authenticated as User3
    )
    assert response.status_code == 403


# --- Tests for GET /expense_splits/{expense_split_id}/settlement_activities ---

@pytest.mark.asyncio
async def test_get_settlement_activities_success(
    client: httpx.AsyncClient,
    test_user1_api: Dict[str, Any], # Group owner / expense creator
    test_user2_api: Dict[str, Any], # User who owes and pays
    test_expense_split_for_user2_api: ExpenseSplit,
    db_session: AsyncSession
):
    user1_headers = test_user1_api["headers"]
    user2 = test_user2_api["user"]
    split = test_expense_split_for_user2_api

    # Create a settlement activity first
    activity_payload = SettlementActivityCreate(expense_split_id=split.id, paid_by_user_id=user2.id, amount_paid=Decimal("10.00"))
    await client.post(
        f"{settings.API_V1_STR}/expense_splits/{split.id}/settle",
        json=activity_payload.model_dump(mode='json'), headers=test_user2_api["headers"] # User2 settles
    )

    # User1 (group owner) fetches activities
    response = await client.get(
        f"{settings.API_V1_STR}/expense_splits/{split.id}/settlement_activities",
        headers=user1_headers
    )
    assert response.status_code == 200
    activities_data = response.json()
    assert isinstance(activities_data, list)
    assert len(activities_data) == 1
    assert activities_data[0]["amount_paid"] == "10.00"
    assert activities_data[0]["paid_by_user_id"] == user2.id

@pytest.mark.asyncio
async def test_get_settlement_activities_split_not_found(
    client: httpx.AsyncClient, test_user1_api: Dict[str, Any]
):
    user1_headers = test_user1_api["headers"]
    response = await client.get(
        f"{settings.API_V1_STR}/expense_splits/9999/settlement_activities",
        headers=user1_headers
    )
    assert response.status_code == 404

@pytest.mark.asyncio
async def test_get_settlement_activities_no_permission(
    client: httpx.AsyncClient,
    test_expense_split_for_user2_api: ExpenseSplit, # Belongs to group of user1/user2
    db_session: AsyncSession
):
    # Create a new user (user3) who is not in the group
    user3 = User(email="api.user3.other@example.com", name="API User 3 Other", hashed_password="password3")
    db_session.add(user3)
    await db_session.commit()
    user3_headers = {"Authorization": f"Bearer token-for-{user3.id}"}

    response = await client.get(
        f"{settings.API_V1_STR}/expense_splits/{test_expense_split_for_user2_api.id}/settlement_activities",
        headers=user3_headers # Authenticated as User3
    )
    assert response.status_code == 403


# --- Test existing expense endpoints for new fields ---
@pytest.mark.asyncio
async def test_get_expense_by_id_includes_new_fields(
    client: httpx.AsyncClient,
    test_user1_api: Dict[str, Any], # User in group
    test_expense_in_group_api: Expense,
    test_expense_split_for_user2_api: ExpenseSplit # one of the splits
):
    user1_headers = test_user1_api["headers"]
    expense_id = test_expense_in_group_api.id

    response = await client.get(f"{settings.API_V1_STR}/expenses/{expense_id}", headers=user1_headers)
    assert response.status_code == 200
    expense_data = response.json()

    assert "overall_settlement_status" in expense_data
    assert expense_data["overall_settlement_status"] == ExpenseOverallStatusEnum.unpaid.value # Initial state

    assert "splits" in expense_data
    assert len(expense_data["splits"]) > 0
    
    found_split = False
    for split_json in expense_data["splits"]:
        if split_json["id"] == test_expense_split_for_user2_api.id:
            found_split = True
            assert "status" in split_json
            assert split_json["status"] == ExpenseSplitStatusEnum.unpaid.value # Initial state
            assert "paid_at" in split_json # Should be null initially
            assert split_json["paid_at"] is None
            assert "settlement_activities" in split_json
            assert isinstance(split_json["settlement_activities"], list)
            assert len(split_json["settlement_activities"]) == 0 # No activities yet
            break
    assert found_split, "The specific test split was not found in the expense data."


# Placeholder for conftest.py content if needed for local execution understanding
"""
# conftest.py (example structure)
import pytest
import asyncio
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.main import app # Your FastAPI app
from app.database import Base, get_transactional_session # Your DB setup

TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"

engine = create_async_engine(TEST_DATABASE_URL, echo=True)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)

@pytest.fixture(scope="session")
def event_loop():
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()

@pytest.fixture(scope="session", autouse=True)
async def setup_db():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)

@pytest.fixture
async def db_session() -> AsyncSession:
    async with TestingSessionLocal() as session:
        # Transaction is handled by get_transactional_session override or test logic
        yield session
        # Rollback changes after test if not using transactional tests per case
        # await session.rollback() # Or rely on test isolation method

@pytest.fixture
async def client(db_session) -> AsyncClient: # Depends on db_session to ensure DB is ready
    async def override_get_transactional_session():
        # Provide the test session, potentially managing transactions per test
        # This is a simplified version; real setup might involve nested transactions
        # or ensuring each test runs in its own transaction that's rolled back.
        try:
            yield db_session 
            # await db_session.commit() # Or commit if test is meant to persist then rollback globally
        except Exception:
            # await db_session.rollback()
            raise
        # finally:
            # await db_session.rollback() # Ensure rollback after each test using this fixture
            
    app.dependency_overrides[get_transactional_session] = override_get_transactional_session
    async with AsyncClient(app=app, base_url="http://test") as c:
        yield c
    del app.dependency_overrides[get_transactional_session] # Clean up
"""
