"""Integration tests for the finance/billing API."""

import pytest
from httpx import AsyncClient
from sqlalchemy import select

from backend.app.core.auth import get_password_hash
from backend.app.core.database import repair_wallet_ledger_internal
from backend.app.models.archive import PrintArchive
from backend.app.models.finance import BudgetReservation, CostCenter, UserWallet, WalletTransaction
from backend.app.models.group import Group
from backend.app.models.print_queue import PrintQueueItem
from backend.app.models.settings import Settings
from backend.app.models.user import User
from backend.app.services.finance_billing import apply_print_charge_for_archive


class TestFinanceAPI:
    @pytest.fixture
    async def admin_user(self, db_session):
        user = User(
            username="finance-admin",
            email="finance-admin@example.com",
            password_hash=get_password_hash("AdminPass1!"),
            role="admin",
            is_active=True,
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)
        return user

    @pytest.fixture
    async def auth_headers(self, async_client: AsyncClient, db_session, admin_user):
        db_session.add(Settings(key="auth_enabled", value="true"))
        db_session.add(Settings(key="advanced_auth_enabled", value="false"))
        # Ensure billing is enabled for finance integration tests
        existing = await db_session.scalar(select(Settings).where(Settings.key == "billing_enabled"))
        if existing is None:
            db_session.add(Settings(key="billing_enabled", value="true"))
        else:
            existing.value = "true"
        await db_session.commit()

        response = await async_client.post(
            "/api/v1/auth/login",
            json={"username": admin_user.username, "password": "AdminPass1!"},
        )
        assert response.status_code == 200
        return {"Authorization": f"Bearer {response.json()['access_token']}"}

    async def _enable_basic_user_creation(self, db_session):
        return None

    async def _create_user_via_api(self, async_client: AsyncClient, auth_headers: dict[str, str], username: str):
        response = await async_client.post(
            "/api/v1/users",
            json={
                "username": username,
                "password": "Regularpass1!",
                "email": f"{username}@example.com",
                "role": "user",
            },
            headers=auth_headers,
        )
        assert response.status_code == 201
        return response.json()

    async def _login_user(self, async_client: AsyncClient, username: str) -> dict[str, str]:
        response = await async_client.post(
            "/api/v1/auth/login",
            json={"username": username, "password": "Regularpass1!"},
        )
        assert response.status_code == 200
        return {"Authorization": f"Bearer {response.json()['access_token']}"}

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_balance_get_does_not_create_wallet(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        target = User(
            username="balance-without-wallet",
            email="balance-without-wallet@example.com",
            password_hash=get_password_hash("Regularpass1!"),
            role="user",
            is_active=True,
        )
        db_session.add(target)
        await db_session.commit()
        await db_session.refresh(target)
        assert await db_session.scalar(select(UserWallet).where(UserWallet.user_id == target.id)) is None

        response = await async_client.get(f"/api/v1/finance/users/{target.id}/balance", headers=auth_headers)

        assert response.status_code == 200
        assert response.json()["balance"] == 0
        assert await db_session.scalar(select(UserWallet).where(UserWallet.user_id == target.id)) is None

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_wallet_ledger_rebuild_processes_more_than_one_batch(
        self,
        db_session,
        admin_user: User,
    ):
        wallet = UserWallet(user_id=admin_user.id, balance=0)
        db_session.add(wallet)
        await db_session.flush()
        await db_session.execute(
            WalletTransaction.__table__.insert(),
            [{"user_id": admin_user.id, "transaction_type": "deposit", "amount": 0.01} for _ in range(1001)],
        )

        await repair_wallet_ledger_internal(db_session)
        await db_session.refresh(wallet)
        last_transaction = await db_session.scalar(
            select(WalletTransaction)
            .where(WalletTransaction.user_id == admin_user.id)
            .order_by(WalletTransaction.created_at.desc(), WalletTransaction.id.desc())
            .limit(1)
        )

        assert wallet.balance == 10.01
        assert last_transaction is not None
        assert last_transaction.balance_after == 10.01

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_create_cost_center_assign_member_and_list_mine(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        await self._enable_basic_user_creation(db_session)
        created_user = await self._create_user_via_api(async_client, auth_headers, "carol")
        user_headers = await self._login_user(async_client, "carol")

        negative_budget_response = await async_client.post(
            "/api/v1/finance/cost-centers",
            json={"name": "Invalid Budget", "total_budget": -1},
            headers=auth_headers,
        )
        assert negative_budget_response.status_code == 422

        create_response = await async_client.post(
            "/api/v1/finance/cost-centers",
            json={
                "name": "Shared Lab",
                "monthly_budget": 120.0,
                "total_budget": 999.0,
                "is_active": True,
            },
            headers=auth_headers,
        )

        assert create_response.status_code == 200
        shared_center = create_response.json()
        assert shared_center["name"] == "Shared Lab"
        assert shared_center["monthly_budget"] == 120.0
        assert shared_center["total_budget"] is None
        assert shared_center["budget_mode"] == "monthly"

        member_response = await async_client.post(
            f"/api/v1/finance/cost-centers/{shared_center['id']}/members",
            json={"user_id": created_user["id"], "can_print": False},
            headers=auth_headers,
        )

        assert member_response.status_code == 200
        assert member_response.json()["user_id"] == created_user["id"]
        assert member_response.json()["can_print"] is False

        mine_response = await async_client.get("/api/v1/finance/cost-centers/mine", headers=user_headers)
        assert mine_response.status_code == 200
        mine_names = {center["name"] for center in mine_response.json()}
        assert "carol" in mine_names
        assert "Shared Lab" in mine_names

        detail_response = await async_client.get(
            f"/api/v1/finance/cost-centers/{shared_center['id']}", headers=auth_headers
        )
        assert detail_response.status_code == 200
        detail = detail_response.json()
        assert len(detail["members"]) == 1
        assert detail["members"][0]["user_id"] == created_user["id"]

        remove_response = await async_client.delete(
            f"/api/v1/finance/cost-centers/{shared_center['id']}/members/{created_user['id']}",
            headers=auth_headers,
        )
        assert remove_response.status_code == 200

        mine_after_remove = await async_client.get("/api/v1/finance/cost-centers/mine", headers=user_headers)
        assert mine_after_remove.status_code == 200
        assert {center["name"] for center in mine_after_remove.json()} == {"carol"}

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_private_cost_center_cannot_be_deactivated_but_can_have_zero_budget(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        created_user = await self._create_user_via_api(async_client, auth_headers, "private-budget-user")
        private_center = await db_session.scalar(
            select(CostCenter).where(
                CostCenter.owner_user_id == created_user["id"],
                CostCenter.is_private.is_(True),
            )
        )
        assert private_center is not None

        deactivate_response = await async_client.patch(
            f"/api/v1/finance/cost-centers/{private_center.id}",
            json={"is_active": False},
            headers=auth_headers,
        )

        assert deactivate_response.status_code == 400
        assert "cannot be deactivated" in deactivate_response.json()["detail"]
        await db_session.refresh(private_center)
        assert private_center.is_active is True

        rename_response = await async_client.patch(
            f"/api/v1/finance/cost-centers/{private_center.id}",
            json={"name": "Renamed private center"},
            headers=auth_headers,
        )
        assert rename_response.status_code == 400
        await db_session.refresh(private_center)
        assert private_center.name == "private-budget-user"

        negative_budget_response = await async_client.patch(
            f"/api/v1/finance/cost-centers/{private_center.id}/budgets",
            json={"monthly_budget": -0.01},
            headers=auth_headers,
        )
        assert negative_budget_response.status_code == 422

        budget_response = await async_client.patch(
            f"/api/v1/finance/cost-centers/{private_center.id}/budgets",
            json={"total_budget": 0},
            headers=auth_headers,
        )

        assert budget_response.status_code == 200
        assert budget_response.json()["total_budget"] == 0

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_cost_center_available_budget_does_not_double_count_queue_reservation(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        center_response = await async_client.post(
            "/api/v1/finance/cost-centers",
            json={"name": "Reserved Once", "total_budget": 10.0},
            headers=auth_headers,
        )
        assert center_response.status_code == 200
        center_id = center_response.json()["id"]

        reserved_item = PrintQueueItem(
            cost_center_id=center_id,
            estimated_cost=3.0,
            status="pending",
            position=1,
        )
        legacy_unreserved_item = PrintQueueItem(
            cost_center_id=center_id,
            estimated_cost=2.0,
            status="pending",
            position=2,
        )
        db_session.add_all([reserved_item, legacy_unreserved_item])
        await db_session.flush()
        db_session.add(
            BudgetReservation(
                cost_center_id=center_id,
                amount=3.0,
                status="active",
                source_type="print_queue",
                source_id=reserved_item.id,
            )
        )
        await db_session.commit()

        response = await async_client.get("/api/v1/finance/cost-centers", headers=auth_headers)

        assert response.status_code == 200
        center = next(item for item in response.json() if item["id"] == center_id)
        # 3.00 active reservation + 2.00 legacy queue estimate, not 3 + 3 + 2.
        assert center["budget_available"] == 5.0

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_cost_center_with_balanced_transactions_cannot_be_deleted(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        admin_user,
        db_session,
    ):
        center_response = await async_client.post(
            "/api/v1/finance/cost-centers",
            json={"name": "Balanced History"},
            headers=auth_headers,
        )
        center_id = center_response.json()["id"]
        db_session.add_all(
            [
                WalletTransaction(
                    user_id=admin_user.id,
                    cost_center_id=center_id,
                    transaction_type="deposit",
                    amount=50.0,
                    balance_after=50.0,
                ),
                WalletTransaction(
                    user_id=admin_user.id,
                    cost_center_id=center_id,
                    transaction_type="withdraw",
                    amount=-50.0,
                    balance_after=0.0,
                ),
            ]
        )
        await db_session.commit()

        response = await async_client.delete(
            f"/api/v1/finance/cost-centers/{center_id}",
            headers=auth_headers,
        )

        assert response.status_code == 400
        assert "transactions reference it" in response.json()["detail"]
        transactions = (
            (await db_session.execute(select(WalletTransaction).where(WalletTransaction.cost_center_id == center_id)))
            .scalars()
            .all()
        )
        assert len(transactions) == 2

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_cost_center_with_active_reservation_cannot_be_deleted(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        center_response = await async_client.post(
            "/api/v1/finance/cost-centers",
            json={"name": "Active Hold"},
            headers=auth_headers,
        )
        center_id = center_response.json()["id"]
        db_session.add(
            BudgetReservation(
                cost_center_id=center_id,
                amount=3.0,
                status="active",
                source_type="direct_print",
                source_id=123,
            )
        )
        await db_session.commit()

        response = await async_client.delete(
            f"/api/v1/finance/cost-centers/{center_id}",
            headers=auth_headers,
        )

        assert response.status_code == 400
        assert "active budget reservations" in response.json()["detail"]
        assert await db_session.get(CostCenter, center_id) is not None

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_wallet_adjustments_and_transaction_ledger_rebuild(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        admin_user,
        db_session,
    ):
        """A user's private cost center and unassigned entries share one wallet."""
        await self._enable_basic_user_creation(db_session)
        created_user = await self._create_user_via_api(async_client, auth_headers, "dave")

        private_center = await db_session.scalar(
            select(CostCenter).where(CostCenter.owner_user_id == created_user["id"], CostCenter.is_private.is_(True))
        )
        assert private_center is not None

        # The owner's private cost center affects both its own ledger and the wallet.
        deposit = await async_client.post(
            f"/api/v1/finance/users/{created_user['id']}/deposit",
            json={"amount": 25.0, "description": "Initial CC top-up", "cost_center_id": private_center.id},
            headers=auth_headers,
        )
        assert deposit.status_code == 200
        assert deposit.json()["transaction"]["cost_center_id"] == private_center.id
        assert deposit.json()["transaction"]["balance_after"] == 25.0  # CC balance
        assert deposit.json()["balance"]["balance"] == 25.0  # Response shows CC balance

        # A withdrawal updates both views by the same amount.
        withdraw = await async_client.post(
            f"/api/v1/finance/users/{created_user['id']}/withdraw",
            json={"amount": 5.0, "description": "CC Usage", "cost_center_id": private_center.id},
            headers=auth_headers,
        )
        assert withdraw.status_code == 200
        assert withdraw.json()["transaction"]["amount"] == -5.0
        assert withdraw.json()["transaction"]["balance_after"] == 20.0  # CC balance after withdraw
        assert withdraw.json()["balance"]["balance"] == 20.0  # Response shows CC balance

        # Personal deposit: affects user wallet
        personal_deposit = await async_client.post(
            f"/api/v1/finance/users/{created_user['id']}/deposit",
            json={"amount": 30.0, "description": "Personal top-up", "cost_center_id": None},
            headers=auth_headers,
        )
        assert personal_deposit.status_code == 200
        assert personal_deposit.json()["transaction"]["cost_center_id"] is None
        assert personal_deposit.json()["transaction"]["balance_after"] == 50.0
        assert personal_deposit.json()["balance"]["balance"] == 50.0

        transactions_response = await async_client.get(
            f"/api/v1/finance/users/{created_user['id']}/transactions", headers=auth_headers
        )
        assert transactions_response.status_code == 200
        transactions = transactions_response.json()
        assert len(transactions) == 3
        cc_txs = [tx for tx in transactions if tx["cost_center_id"] == private_center.id]
        personal_txs = [tx for tx in transactions if tx["cost_center_id"] is None]
        assert len(cc_txs) == 2
        assert len(personal_txs) == 1

        balance_response = await async_client.get(
            f"/api/v1/finance/users/{created_user['id']}/balance", headers=auth_headers
        )
        assert balance_response.status_code == 200
        assert balance_response.json()["balance"] == 50.0

        # Delete personal transaction, user wallet should decrease
        personal_tx = next(tx for tx in transactions if tx["cost_center_id"] is None)
        delete_response = await async_client.delete(
            f"/api/v1/finance/transactions/{personal_tx['id']}", headers=auth_headers
        )
        assert delete_response.status_code == 200

        balance_after_delete = await async_client.get(
            f"/api/v1/finance/users/{created_user['id']}/balance", headers=auth_headers
        )
        assert balance_after_delete.status_code == 200
        assert balance_after_delete.json()["balance"] == 20.0

        remaining = await async_client.get(
            f"/api/v1/finance/users/{created_user['id']}/transactions", headers=auth_headers
        )
        assert remaining.status_code == 200
        assert len(remaining.json()) == 2  # 2 CC transactions remain

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_admin_and_user_balances_agree_after_charge_adjustment_and_delete(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        created_user = await self._create_user_via_api(async_client, auth_headers, "balance-lifecycle")
        user = await db_session.get(User, created_user["id"])
        operators = await db_session.scalar(select(Group).where(Group.name == "Operators"))
        assert operators is not None
        user.groups.append(operators)
        await db_session.commit()
        user_headers = await self._login_user(async_client, "balance-lifecycle")
        private_center = await db_session.scalar(
            select(CostCenter).where(CostCenter.owner_user_id == user.id, CostCenter.is_private.is_(True))
        )
        assert private_center is not None

        deposit = await async_client.post(
            f"/api/v1/finance/users/{user.id}/deposit",
            json={"amount": 20.0, "cost_center_id": private_center.id},
            headers=auth_headers,
        )
        assert deposit.status_code == 200

        archive = PrintArchive(
            filename="charged.gcode.3mf",
            file_path="archives/test/charged.gcode.3mf",
            file_size=10,
            status="completed",
            cost=4.0,
            created_by_id=user.id,
            cost_center_id=private_center.id,
        )
        db_session.add(archive)
        await db_session.commit()
        assert await apply_print_charge_for_archive(db_session, archive.id) is True
        await db_session.commit()

        adjustment = await async_client.post(
            f"/api/v1/finance/users/{user.id}/deposit",
            json={"amount": 5.0, "description": "temporary adjustment"},
            headers=auth_headers,
        )
        assert adjustment.status_code == 200

        async def assert_views_agree(expected: float):
            admin_view = await async_client.get(
                f"/api/v1/finance/users/{user.id}/balance",
                headers=auth_headers,
            )
            user_view = await async_client.get("/api/v1/finance/me/balance", headers=user_headers)
            assert admin_view.status_code == 200
            assert user_view.status_code == 200
            assert admin_view.json()["balance"] == expected
            assert user_view.json()["balance"] == expected

        await assert_views_agree(21.0)

        delete_response = await async_client.delete(
            f"/api/v1/finance/transactions/{adjustment.json()['transaction']['id']}",
            headers=auth_headers,
        )
        assert delete_response.status_code == 200
        await assert_views_agree(16.0)

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_delete_cost_center_transaction_rebuilds_remaining_ledger(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        await self._enable_basic_user_creation(db_session)
        created_user = await self._create_user_via_api(async_client, auth_headers, "erin")

        shared_center = await db_session.scalar(
            select(CostCenter).where(CostCenter.owner_user_id == created_user["id"], CostCenter.is_private.is_(True))
        )
        assert shared_center is not None

        first_deposit = await async_client.post(
            f"/api/v1/finance/users/{created_user['id']}/deposit",
            json={"amount": 25.0, "description": "CC top-up", "cost_center_id": shared_center.id},
            headers=auth_headers,
        )
        assert first_deposit.status_code == 200

        cc_withdraw = await async_client.post(
            f"/api/v1/finance/users/{created_user['id']}/withdraw",
            json={"amount": 5.0, "description": "CC usage", "cost_center_id": shared_center.id},
            headers=auth_headers,
        )
        assert cc_withdraw.status_code == 200

        personal_deposit = await async_client.post(
            f"/api/v1/finance/users/{created_user['id']}/deposit",
            json={"amount": 12.0, "description": "Personal top-up", "cost_center_id": None},
            headers=auth_headers,
        )
        assert personal_deposit.status_code == 200

        delete_response = await async_client.delete(
            f"/api/v1/finance/transactions/{first_deposit.json()['transaction']['id']}",
            headers=auth_headers,
        )
        assert delete_response.status_code == 200

        transactions_response = await async_client.get(
            f"/api/v1/finance/users/{created_user['id']}/transactions", headers=auth_headers
        )
        assert transactions_response.status_code == 200
        transactions = transactions_response.json()
        assert len(transactions) == 2

        cc_transaction = next(tx for tx in transactions if tx["cost_center_id"] == shared_center.id)
        assert cc_transaction["amount"] == -5.0
        assert cc_transaction["balance_after"] == -5.0

        balance_response = await async_client.get(
            f"/api/v1/finance/users/{created_user['id']}/balance", headers=auth_headers
        )
        assert balance_response.status_code == 200
        assert balance_response.json()["balance"] == 7.0

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_delete_print_charge_stays_deleted_after_recalculate(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        await self._enable_basic_user_creation(db_session)
        created_user = await self._create_user_via_api(async_client, auth_headers, "frank")
        user = await db_session.scalar(select(User).where(User.id == created_user["id"]))
        assert user is not None
        user_id = user.id

        archive = PrintArchive(
            printer_id=None,
            filename="print.gcode",
            file_path="archives/test/print.gcode",
            file_size=10,
            content_hash="hash-print",
            status="completed",
            cost=4.0,
            created_by_id=user.id,
        )
        db_session.add(archive)
        await db_session.flush()

        tx = WalletTransaction(
            user_id=user.id,
            transaction_type="print_charge",
            amount=-4.0,
            balance_after=-4.0,
            description="Print charge: print.gcode",
            created_by_user_id=None,
            print_run_id="deleted-print-run",
            print_archive_id=archive.id,
        )
        db_session.add(tx)
        await db_session.commit()
        archive_id = archive.id

        tx_rows_before = (
            (await db_session.execute(select(WalletTransaction).where(WalletTransaction.user_id == user_id)))
            .scalars()
            .all()
        )
        assert len(tx_rows_before) == 1

        delete_response = await async_client.delete(
            f"/api/v1/finance/transactions/{tx_rows_before[0].id}", headers=auth_headers
        )
        assert delete_response.status_code == 200
        db_session.expire_all()

        tx_rows_after = (
            (await db_session.execute(select(WalletTransaction).where(WalletTransaction.user_id == user_id)))
            .scalars()
            .all()
        )
        assert len(tx_rows_after) == 1
        assert tx_rows_after[0].is_voided is True

        # The voided run remains an idempotency tombstone and cannot be
        # recreated by a delayed duplicate completion callback.
        assert (
            await apply_print_charge_for_archive(
                db_session,
                archive_id,
                print_run_id="deleted-print-run",
            )
        ) is False

        # A later reprint of the same archive has a distinct run identity and
        # must still be charged normally.
        assert (
            await apply_print_charge_for_archive(
                db_session,
                archive_id,
                charged_user_id=user_id,
                print_run_id="later-reprint-run",
            )
        ) is True
        await db_session.commit()
        visible = await async_client.get(
            f"/api/v1/finance/users/{user_id}/transactions",
            headers=auth_headers,
        )
        assert visible.status_code == 200
        assert [row["print_run_id"] for row in visible.json()] == ["later-reprint-run"]

    async def test_edit_transaction_updates_ledger(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        """Test that editing a transaction (user, cost_center, amount, description) rebuilds ledger."""
        await self._enable_basic_user_creation(db_session)
        user1 = await self._create_user_via_api(async_client, auth_headers, "user1")
        user2 = await self._create_user_via_api(async_client, auth_headers, "user2")

        # Create a cost center
        cc_response = await async_client.post(
            "/api/v1/finance/cost-centers",
            json={"name": "Test Center", "is_active": True},
            headers=auth_headers,
        )
        assert cc_response.status_code == 200
        cost_center = cc_response.json()

        # Get user records from DB
        user1_db = await db_session.scalar(select(User).where(User.id == user1["id"]))
        user2_db = await db_session.scalar(select(User).where(User.id == user2["id"]))

        # Create a personal transaction for user1
        tx_response = await async_client.post(
            f"/api/v1/finance/users/{user1_db.id}/deposit",
            json={"amount": 50.0, "description": "Initial deposit"},
            headers=auth_headers,
        )
        assert tx_response.status_code == 200
        tx_data = tx_response.json()
        tx_id = tx_data["transaction"]["id"]

        # Get the original transaction
        original_tx = await db_session.scalar(select(WalletTransaction).where(WalletTransaction.id == tx_id))
        assert original_tx.user_id == user1_db.id
        assert original_tx.cost_center_id is None
        assert original_tx.amount == 50.0
        assert original_tx.balance_after == 50.0

        # Edit the transaction: change user, add cost center, change amount
        edit_response = await async_client.patch(
            f"/api/v1/finance/transactions/{tx_id}",
            json={
                "user_id": user2_db.id,
                "cost_center_id": cost_center["id"],
                "amount": 75.0,
                "description": "Updated deposit (Admin edit)",
            },
            headers=auth_headers,
        )
        assert edit_response.status_code == 200
        edited_tx_data = edit_response.json()

        # Verify transaction was updated
        assert edited_tx_data["user_id"] == user2_db.id
        assert edited_tx_data["cost_center_id"] == cost_center["id"]
        assert edited_tx_data["amount"] == 75.0
        # Description should have "(Admin edit)" appended
        assert "(Admin edit)" in edited_tx_data["description"]

        # An explicit null moves the transaction back to the personal ledger.
        clear_response = await async_client.patch(
            f"/api/v1/finance/transactions/{tx_id}",
            json={"cost_center_id": None},
            headers=auth_headers,
        )
        assert clear_response.status_code == 200
        assert clear_response.json()["cost_center_id"] is None

        invalid_user_response = await async_client.patch(
            f"/api/v1/finance/transactions/{tx_id}",
            json={"user_id": 2147483647},
            headers=auth_headers,
        )
        assert invalid_user_response.status_code == 404
        assert invalid_user_response.json()["detail"] == "User not found"

        invalid_center_response = await async_client.patch(
            f"/api/v1/finance/transactions/{tx_id}",
            json={"cost_center_id": 2147483647},
            headers=auth_headers,
        )
        assert invalid_center_response.status_code == 404
        assert invalid_center_response.json()["detail"] == "Cost center not found"

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_create_manual_print_and_recalculates_ledger(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        """Posting a manual print (manual_adjustment) creates a transaction and rebuilds ledger."""
        await self._enable_basic_user_creation(db_session)
        created_user = await self._create_user_via_api(async_client, auth_headers, "gina")

        # Get user DB record
        user = await db_session.scalar(select(User).where(User.id == created_user["id"]))
        assert user is not None

        # Private cost center for user
        private_cc = await db_session.scalar(
            select(CostCenter).where(CostCenter.owner_user_id == created_user["id"], CostCenter.is_private.is_(True))
        )
        assert private_cc is not None

        # Post manual print affecting the cost center
        payload = {
            "user_id": user.id,
            "cost_center_id": private_cc.id,
            "amount": 4.0,
            "description": "Manual adjustment for a print",
            "created_at": "2026-05-12T12:00:00Z",
        }

        response = await async_client.post("/api/v1/finance/transactions/manual", json=payload, headers=auth_headers)
        assert response.status_code == 200
        resp_json = response.json()
        assert "transaction" in resp_json or "id" in resp_json

        # Response contains the created transaction details
        assert resp_json["transaction_type"] == "manual_adjustment"
        assert resp_json["amount"] == -4.0
        assert resp_json["cost_center_id"] == private_cc.id

        # The response includes the computed running balance for the transaction
        assert resp_json.get("balance_after") == -4.0

        negative_amount_response = await async_client.post(
            "/api/v1/finance/transactions/manual",
            json={**payload, "amount": -1},
            headers=auth_headers,
        )
        assert negative_amount_response.status_code == 422

        invalid_user_response = await async_client.post(
            "/api/v1/finance/transactions/manual",
            json={**payload, "user_id": 2147483647},
            headers=auth_headers,
        )
        assert invalid_user_response.status_code == 404
        assert invalid_user_response.json()["detail"] == "User not found"

        invalid_center_response = await async_client.post(
            "/api/v1/finance/transactions/manual",
            json={**payload, "cost_center_id": 2147483647},
            headers=auth_headers,
        )
        assert invalid_center_response.status_code == 404
        assert invalid_center_response.json()["detail"] == "Cost center not found"


class TestPartialPrintChargesIntegration:
    """Integration tests for partial print charge calculation."""

    @pytest.fixture
    async def admin_user(self, db_session):
        user = User(
            username="partial-admin",
            email="partial-admin@example.com",
            password_hash=get_password_hash("AdminPass1!"),
            role="admin",
            is_active=True,
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)
        return user

    @pytest.fixture
    async def auth_headers(self, async_client: AsyncClient, db_session, admin_user):
        db_session.add(Settings(key="auth_enabled", value="true"))
        db_session.add(Settings(key="advanced_auth_enabled", value="false"))
        # Ensure billing is enabled for these partial-charge integration tests
        existing = await db_session.scalar(select(Settings).where(Settings.key == "billing_enabled"))
        if existing is None:
            db_session.add(Settings(key="billing_enabled", value="true"))
        else:
            existing.value = "true"
        await db_session.commit()

        response = await async_client.post(
            "/api/v1/auth/login",
            json={"username": admin_user.username, "password": "AdminPass1!"},
        )
        assert response.status_code == 200
        return {"Authorization": f"Bearer {response.json()['access_token']}"}

    async def _create_user_via_api(self, async_client: AsyncClient, auth_headers: dict[str, str], username: str):
        response = await async_client.post(
            "/api/v1/users",
            json={
                "username": username,
                "password": "Regularpass1!",
                "email": f"{username}@example.com",
                "role": "user",
            },
            headers=auth_headers,
        )
        assert response.status_code == 201
        return response.json()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_aborted_print_charges_proportionally_via_recalculate_endpoint(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        """Verify aborted prints are included in recalculate and charged proportionally."""
        created_user = await self._create_user_via_api(async_client, auth_headers, "frank")
        user = await db_session.scalar(select(User).where(User.id == created_user["id"]))
        assert user is not None

        # Wallet is already created by ensure_user_finance_defaults during user creation

        # Archive: completed print (100% charge)
        completed = PrintArchive(
            printer_id=None,
            filename="completed.3mf",
            file_path="archives/test/completed.3mf",
            file_size=100,
            content_hash="partial-complete",
            status="completed",
            cost=10.0,
            created_by_id=user.id,
        )

        # Archive: aborted print (50% filament used = 50% charge)
        aborted = PrintArchive(
            printer_id=None,
            filename="aborted.3mf",
            file_path="archives/test/aborted.3mf",
            file_size=100,
            content_hash="partial-aborted",
            status="aborted",
            cost=8.0,
            filament_used_grams=50.0,
            extra_data={"filament_grams_total": 100.0},
            created_by_id=user.id,
        )

        # Archive: failed print (0% filament used = no charge)
        failed = PrintArchive(
            printer_id=None,
            filename="failed.3mf",
            file_path="archives/test/failed.3mf",
            file_size=100,
            content_hash="partial-failed",
            status="failed",
            cost=5.0,
            filament_used_grams=0.0,
            created_by_id=user.id,
        )

        db_session.add_all([completed, aborted, failed])
        await db_session.commit()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_partial_charges_appear_in_transaction_ledger(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        """Verify transaction descriptions indicate partial charges."""
        created_user = await self._create_user_via_api(async_client, auth_headers, "grace")
        user = await db_session.scalar(select(User).where(User.id == created_user["id"]))
        assert user is not None

        # Wallet is already created by ensure_user_finance_defaults during user creation

        cancelled = PrintArchive(
            printer_id=None,
            filename="cancelled.3mf",
            file_path="archives/test/cancelled.3mf",
            file_size=100,
            content_hash="partial-cancel",
            status="cancelled",
            cost=12.0,
            filament_used_grams=25.0,
            extra_data={"filament_grams_total": 100.0},
            print_name="Partially Cancelled Print",
            created_by_id=user.id,
        )
        db_session.add(cancelled)
        await db_session.commit()

        from backend.app.services.finance_billing import apply_print_charge_for_archive

        changed = await apply_print_charge_for_archive(db_session, cancelled.id)
        assert changed is True
        await db_session.commit()

        tx_response = await async_client.get(
            f"/api/v1/finance/users/{created_user['id']}/transactions", headers=auth_headers
        )
        assert tx_response.status_code == 200
        transactions = tx_response.json()
        assert len(transactions) == 1

        tx = transactions[0]
        assert tx["transaction_type"] == "print_charge"
        assert tx["amount"] == -3.0  # 25% of 12.0
        assert "cancelled" in tx["description"].lower()
        assert "25.0g/100.0" in tx["description"]  # filament amounts in description

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_partial_charges_with_cost_center_override(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        """Verify partial charges respect cost_center_id when present."""
        created_user = await self._create_user_via_api(async_client, auth_headers, "henry")
        user = await db_session.scalar(select(User).where(User.id == created_user["id"]))
        assert user is not None

        # Create cost centers
        default_cc = CostCenter(name="Default CC", owner_user_id=user.id, is_active=True, is_private=False)
        lab_cc = CostCenter(name="Lab CC", owner_user_id=user.id, is_active=True, is_private=False)
        db_session.add_all([default_cc, lab_cc])
        await db_session.flush()

        # Wallet is already created by ensure_user_finance_defaults during user creation

        # Archive assigned to default_cc
        aborted = PrintArchive(
            printer_id=None,
            filename="aborted_cc.3mf",
            file_path="archives/test/aborted_cc.3mf",
            file_size=100,
            content_hash="partial-cc",
            status="aborted",
            cost=6.0,
            filament_used_grams=30.0,
            extra_data={"filament_grams_total": 100.0},
            cost_center_id=default_cc.id,
            created_by_id=user.id,
        )
        db_session.add(aborted)
        await db_session.commit()

        # Manually apply charge with override
        from backend.app.services.finance_billing import apply_print_charge_for_archive

        changed = await apply_print_charge_for_archive(
            db_session,
            aborted.id,
            cost_center_id=lab_cc.id,
        )
        await db_session.commit()

        assert changed is True

        tx_response = await async_client.get(
            f"/api/v1/finance/users/{created_user['id']}/transactions", headers=auth_headers
        )
        assert tx_response.status_code == 200
        transactions = tx_response.json()
        assert len(transactions) == 1

        tx = transactions[0]
        assert tx["cost_center_id"] == lab_cc.id  # Overridden to lab_cc
        assert tx["amount"] == pytest.approx(-1.8, abs=0.01)  # 30% of 6.0


class TestFinanceUserDefaults:
    """Tests for user creation and finance defaults initialization."""

    @pytest.fixture
    async def admin_user(self, db_session):
        user = User(
            username="billing-admin",
            email="billing-admin@example.com",
            password_hash=get_password_hash("AdminPass1!"),
            role="admin",
            is_active=True,
        )
        db_session.add(user)
        await db_session.commit()
        await db_session.refresh(user)
        return user

    @pytest.fixture
    async def auth_headers(self, async_client: AsyncClient, db_session, admin_user):
        db_session.add(Settings(key="auth_enabled", value="true"))
        db_session.add(Settings(key="advanced_auth_enabled", value="false"))
        await db_session.commit()

        response = await async_client.post(
            "/api/v1/auth/login",
            json={"username": admin_user.username, "password": "AdminPass1!"},
        )
        assert response.status_code == 200
        return {"Authorization": f"Bearer {response.json()['access_token']}"}

    async def _create_user_via_api(self, async_client: AsyncClient, auth_headers: dict[str, str], username: str):
        response = await async_client.post(
            "/api/v1/users",
            json={
                "username": username,
                "password": "Regularpass1!",
                "email": f"{username}@example.com",
                "role": "user",
            },
            headers=auth_headers,
        )
        assert response.status_code == 201
        return response.json()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_create_user_initializes_wallet_and_private_cost_center(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        """Verify user creation initializes wallet, private cost center, and membership."""
        result = await async_client.post(
            "/api/v1/users",
            json={
                "username": "alice",
                "password": "Regularpass1!",
                "email": "alice@example.com",
                "role": "user",
            },
            headers=auth_headers,
        )

        assert result.status_code == 201
        created = result.json()
        assert created["username"] == "alice"

        user = await db_session.scalar(select(User).where(User.username == "alice"))
        assert user is not None

        from backend.app.models.finance import CostCenterMember

        wallet = await db_session.scalar(select(UserWallet).where(UserWallet.user_id == user.id))
        assert wallet is not None
        assert wallet.balance == 0.0

        private_center = await db_session.scalar(
            select(CostCenter).where(CostCenter.owner_user_id == user.id, CostCenter.is_private.is_(True))
        )
        assert private_center is not None
        assert private_center.name == "alice"

        membership = await db_session.scalar(
            select(CostCenterMember).where(
                CostCenterMember.cost_center_id == private_center.id,
                CostCenterMember.user_id == user.id,
            )
        )
        assert membership is not None
        assert membership.can_print is True

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_update_user_keeps_private_cost_center_in_sync(
        self,
        async_client: AsyncClient,
        auth_headers: dict[str, str],
        db_session,
    ):
        """Verify user updates keep private cost center name in sync."""
        created = await self._create_user_via_api(async_client, auth_headers, "bob")

        response = await async_client.patch(
            f"/api/v1/users/{created['id']}",
            json={"username": "bobby"},
            headers=auth_headers,
        )

        assert response.status_code == 200
        assert response.json()["username"] == "bobby"

        user = await db_session.scalar(select(User).where(User.id == created["id"]))
        assert user is not None

        private_centers = (
            (
                await db_session.execute(
                    select(CostCenter).where(CostCenter.owner_user_id == user.id, CostCenter.is_private.is_(True))
                )
            )
            .scalars()
            .all()
        )
        assert len(private_centers) == 1
        assert private_centers[0].name == "bobby"

        wallet = await db_session.scalar(select(UserWallet).where(UserWallet.user_id == user.id))
        assert wallet is not None
