import json
from datetime import date
from decimal import Decimal
import os
import sys
import types
import warnings
from pathlib import Path

import pytest
from fastapi import FastAPI
from sqlalchemy import create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool


ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
os.environ.setdefault("DB_NAME", ":memory:.db")

from app.core.database import Base
from app.core.models import Customer, Expense, GstAdjustment, GstReturn, GstReturnSnapshotRow, Invoice, InvoiceItem, InvoiceStatus, Payment, Tenant
from app.services.gst_service import build_gst_return_draft, create_gst_adjustment, delete_gst_adjustment, delete_gst_return, file_gst_return, lock_gst_return, prepare_gst_return, save_gst_return_draft


def make_db():
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    db = sessionmaker(bind=engine)()
    db.add(Tenant(id=1, company_code="test", name="Test"))
    db.commit()
    return db


def seed_cash_basis_activity(db):
    customer = Customer(tenant_id=1, name="Snapshot Customer", company_name="Snapshot Co")
    db.add(customer)
    db.flush()

    invoice = Invoice(
        tenant_id=1,
        invoice_number="INV-SNAP-001",
        customer_id=customer.id,
        invoice_date=date(2026, 4, 1),
        subtotal=Decimal("1000.00"),
        total_tax=Decimal("150.00"),
        total_amount=Decimal("1150.00"),
        status=InvoiceStatus.sent,
    )
    db.add(invoice)
    db.flush()
    db.add(
        InvoiceItem(
            tenant_id=1,
            invoice_id=invoice.id,
            description="Snapshot work",
            quantity=Decimal("1"),
            unit_price=Decimal("1000.00"),
            tax_mode="gst_15",
            tax_rate=Decimal("0.1500"),
            subtotal=Decimal("1000.00"),
            tax_amount=Decimal("150.00"),
            total=Decimal("1150.00"),
        )
    )
    payment = Payment(tenant_id=1, invoice_id=invoice.id, received_date=date(2026, 4, 3), amount=Decimal("575.00"))
    db.add(payment)

    expense = Expense(
        tenant_id=1,
        expense_date=date(2026, 4, 4),
        vendor_name="Snapshot Supplier",
        category="Supplies",
        description="Paper",
        amount_net=Decimal("200.00"),
        gst_amount=Decimal("30.00"),
        amount_gross=Decimal("230.00"),
        tax_mode="gst_15",
        gst_claimable=True,
        status="confirmed",
    )
    db.add(expense)
    db.commit()
    return payment, expense


def test_prepare_gst_return_persists_totals_and_snapshot_rows() -> None:
    db = make_db()
    seed_cash_basis_activity(db)

    gst_return = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    assert gst_return.id is not None
    assert gst_return.status == "prepared"
    assert gst_return.period_start == date(2026, 4, 1)
    assert gst_return.period_end == date(2026, 4, 30)
    assert gst_return.total_sales_income == Decimal("575.00")
    assert gst_return.gst_output == Decimal("75.00")
    assert gst_return.total_purchases_expenses == Decimal("230.00")
    assert gst_return.gst_input == Decimal("30.00")
    assert gst_return.gst_payable == Decimal("45.00")
    assert gst_return.prepared_at is not None

    rows = db.query(GstReturnSnapshotRow).filter_by(gst_return_id=gst_return.id).order_by(GstReturnSnapshotRow.id).all()
    assert [row.row_type for row in rows] == ["payment", "expense"]
    assert rows[0].gross_amount == Decimal("575.00")
    assert rows[0].gst_amount == Decimal("75.00")
    assert rows[0].customer_or_vendor == "Snapshot Co"
    assert rows[1].gross_amount == Decimal("230.00")
    assert rows[1].gst_amount == Decimal("30.00")
    assert rows[1].customer_or_vendor == "Snapshot Supplier"


def test_save_gst_return_draft_can_update_and_then_prepare_same_period() -> None:
    db = make_db()
    payment, _expense = seed_cash_basis_activity(db)

    draft = save_gst_return_draft(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)
    payment.amount = Decimal("1150.00")
    db.commit()
    updated_draft = save_gst_return_draft(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)
    prepared = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    assert draft.id == updated_draft.id == prepared.id
    assert prepared.status == "prepared"
    assert prepared.total_sales_income == Decimal("1150.00")
    assert prepared.gst_output == Decimal("150.00")
    rows = db.query(GstReturnSnapshotRow).filter_by(gst_return_id=prepared.id).order_by(GstReturnSnapshotRow.id).all()
    assert [row.row_type for row in rows] == ["payment", "expense"]
    assert rows[0].gross_amount == Decimal("1150.00")


def test_save_or_prepare_existing_unfiled_return_reuses_same_record_without_conflict() -> None:
    db = make_db()
    payment, _expense = seed_cash_basis_activity(db)
    prepared = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)
    locked = lock_gst_return(db, prepared.id, tenant_id=1)

    payment.amount = Decimal("1150.00")
    db.commit()
    draft = save_gst_return_draft(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)
    prepared_again = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    assert locked.id == draft.id == prepared_again.id
    assert draft.status == "prepared"
    assert prepared_again.status == "prepared"
    assert prepared_again.total_sales_income == Decimal("1150.00")
    assert prepared_again.gst_output == Decimal("150.00")
    assert prepared_again.locked_at is None
    assert db.query(GstReturn).count() == 1


def test_save_or_prepare_filed_return_is_not_overwritten() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    filed = file_gst_return(db, prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1).id, tenant_id=1)

    with pytest.raises(ValueError, match="Filed GST returns cannot be updated"):
        save_gst_return_draft(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)
    with pytest.raises(ValueError, match="Filed GST returns cannot be updated"):
        prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    assert db.query(GstReturn).count() == 1
    assert db.query(GstReturn).first().id == filed.id


def test_delete_gst_return_allows_draft_prepared_and_locked_but_rejects_filed() -> None:
    db = make_db()
    seed_cash_basis_activity(db)

    draft = save_gst_return_draft(db, date(2026, 1, 1), date(2026, 1, 31), tenant_id=1)
    prepared = prepare_gst_return(db, date(2026, 2, 1), date(2026, 2, 28), tenant_id=1)
    locked = lock_gst_return(db, prepare_gst_return(db, date(2026, 3, 1), date(2026, 3, 31), tenant_id=1).id, tenant_id=1)
    filed = file_gst_return(db, prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1).id, tenant_id=1)

    assert delete_gst_return(db, draft.id, tenant_id=1)["status"] == "draft"
    assert delete_gst_return(db, prepared.id, tenant_id=1)["status"] == "prepared"
    assert delete_gst_return(db, locked.id, tenant_id=1)["status"] == "locked"
    with pytest.raises(ValueError, match="Filed GST returns cannot be deleted"):
        delete_gst_return(db, filed.id, tenant_id=1)

    remaining = db.query(GstReturn).all()
    assert [item.id for item in remaining] == [filed.id]


def test_create_gst_adjustments_are_included_in_target_period_draft() -> None:
    db = make_db()
    payment, expense = seed_cash_basis_activity(db)

    debit = create_gst_adjustment(
        db,
        tenant_id=1,
        adjustment_type="debit",
        reason="Under-reported output GST",
        amount=Decimal("12.34"),
        target_period_start=date(2026, 4, 1),
        target_period_end=date(2026, 4, 30),
        source_period_start=date(2026, 3, 1),
        source_period_end=date(2026, 3, 31),
        linked_invoice_id=payment.invoice_id,
        linked_payment_id=payment.id,
    )
    credit = create_gst_adjustment(
        db,
        tenant_id=1,
        adjustment_type="credit",
        reason="Over-claimed input correction",
        amount=Decimal("5.50"),
        target_period_start=date(2026, 4, 1),
        target_period_end=date(2026, 4, 30),
        linked_expense_id=expense.id,
    )

    draft = build_gst_return_draft(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    assert debit.id is not None
    assert credit.id is not None
    assert db.query(GstAdjustment).count() == 2
    assert draft["debit_adjustments"] == Decimal("12.34")
    assert draft["credit_adjustments"] == Decimal("5.50")
    assert draft["total_gst_collected"] == Decimal("87.34")
    assert draft["total_gst_purchases"] == Decimal("35.50")
    assert draft["gst_payable"] == Decimal("51.84")
    assert [(row["description"], row["gst_amount"]) for row in draft["adjustment_rows"]] == [
        ("Under-reported output GST", Decimal("12.34")),
        ("Over-claimed input correction", Decimal("5.50")),
    ]
    metadata = json.loads(draft["adjustment_rows"][0]["metadata_json"])
    assert metadata["linked_invoice_id"] == payment.invoice_id
    assert metadata["linked_payment_id"] == payment.id
    assert metadata["linked_expense_id"] is None
    assert metadata["source_period_start"] == "2026-03-01"
    assert metadata["source_period_end"] == "2026-03-31"
    assert metadata["target_period_start"] == "2026-04-01"
    assert metadata["target_period_end"] == "2026-04-30"


def test_delete_gst_adjustment_removes_it_from_draft() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    adjustment = create_gst_adjustment(
        db,
        tenant_id=1,
        adjustment_type="debit",
        reason="Remove this draft correction",
        amount=Decimal("12.34"),
        target_period_start=date(2026, 4, 1),
        target_period_end=date(2026, 4, 30),
    )

    deleted = delete_gst_adjustment(db, adjustment.id, tenant_id=1)
    draft = build_gst_return_draft(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    assert deleted["id"] == adjustment.id
    assert db.query(GstAdjustment).count() == 0
    assert draft["debit_adjustments"] == Decimal("0.00")
    assert draft["adjustment_rows"] == []


def test_prepare_gst_return_snapshot_keeps_adjustment_trace_metadata() -> None:
    db = make_db()
    payment, expense = seed_cash_basis_activity(db)
    create_gst_adjustment(
        db,
        tenant_id=1,
        adjustment_type="debit",
        reason="Traceable correction",
        amount=Decimal("7.25"),
        target_period_start=date(2026, 4, 1),
        target_period_end=date(2026, 4, 30),
        source_period_start=date(2026, 3, 1),
        source_period_end=date(2026, 3, 31),
        linked_invoice_id=payment.invoice_id,
        linked_payment_id=payment.id,
        linked_expense_id=expense.id,
    )

    gst_return = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    adjustment_row = (
        db.query(GstReturnSnapshotRow)
        .filter_by(gst_return_id=gst_return.id, row_type="adjustment")
        .one()
    )
    metadata = json.loads(adjustment_row.metadata_json)
    assert metadata == {
        "adjustment_type": "debit",
        "direction": "debit",
        "linked_expense_id": expense.id,
        "linked_invoice_id": payment.invoice_id,
        "linked_payment_id": payment.id,
        "source_id": None,
        "source_period_end": "2026-03-31",
        "source_period_start": "2026-03-01",
        "source_type": None,
        "target_period_end": "2026-04-30",
        "target_period_start": "2026-04-01",
    }


@pytest.mark.parametrize(
    ("adjustment_type", "amount", "message"),
    [
        ("output", Decimal("1.00"), "adjustment_type must be debit or credit"),
        ("debit", Decimal("0.00"), "amount must be greater than 0"),
        ("credit", Decimal("-0.01"), "amount must be greater than 0"),
    ],
)
def test_create_gst_adjustment_rejects_invalid_type_and_amount(adjustment_type: str, amount: Decimal, message: str) -> None:
    db = make_db()

    with pytest.raises(ValueError, match=message):
        create_gst_adjustment(
            db,
            tenant_id=1,
            adjustment_type=adjustment_type,
            reason="Invalid adjustment",
            amount=amount,
            target_period_start=date(2026, 4, 1),
            target_period_end=date(2026, 4, 30),
        )

    assert db.query(GstAdjustment).count() == 0


def test_create_gst_adjustment_rejects_blank_reason_and_partial_source_period() -> None:
    db = make_db()

    with pytest.raises(ValueError, match="reason is required"):
        create_gst_adjustment(
            db,
            tenant_id=1,
            adjustment_type="debit",
            reason="   ",
            amount=Decimal("1.00"),
            target_period_start=date(2026, 4, 1),
            target_period_end=date(2026, 4, 30),
        )

    with pytest.raises(ValueError, match="source_period_start and source_period_end must be provided together"):
        create_gst_adjustment(
            db,
            tenant_id=1,
            adjustment_type="credit",
            reason="Partial source",
            amount=Decimal("1.00"),
            target_period_start=date(2026, 4, 1),
            target_period_end=date(2026, 4, 30),
            source_period_start=date(2026, 3, 1),
        )

    with pytest.raises(ValueError, match="source_period_end must be on or after source_period_start"):
        create_gst_adjustment(
            db,
            tenant_id=1,
            adjustment_type="credit",
            reason="Backwards source",
            amount=Decimal("1.00"),
            target_period_start=date(2026, 4, 1),
            target_period_end=date(2026, 4, 30),
            source_period_start=date(2026, 3, 31),
            source_period_end=date(2026, 3, 1),
        )

    assert db.query(GstAdjustment).count() == 0


def test_create_gst_adjustment_validates_linked_sources() -> None:
    db = make_db()
    payment, expense = seed_cash_basis_activity(db)
    other_customer = Customer(tenant_id=1, name="Other Customer")
    db.add(other_customer)
    db.flush()
    other_invoice = Invoice(
        tenant_id=1,
        invoice_number="INV-OTHER",
        customer_id=other_customer.id,
        invoice_date=date(2026, 4, 2),
        total_amount=Decimal("10.00"),
        status=InvoiceStatus.sent,
    )
    db.add(other_invoice)
    db.commit()

    with pytest.raises(LookupError, match="linked invoice not found"):
        create_gst_adjustment(
            db,
            tenant_id=1,
            adjustment_type="debit",
            reason="Missing invoice",
            amount=Decimal("1.00"),
            target_period_start=date(2026, 4, 1),
            target_period_end=date(2026, 4, 30),
            linked_invoice_id=999,
        )
    with pytest.raises(LookupError, match="linked payment not found"):
        create_gst_adjustment(
            db,
            tenant_id=1,
            adjustment_type="debit",
            reason="Missing payment",
            amount=Decimal("1.00"),
            target_period_start=date(2026, 4, 1),
            target_period_end=date(2026, 4, 30),
            linked_payment_id=999,
        )
    with pytest.raises(LookupError, match="linked expense not found"):
        create_gst_adjustment(
            db,
            tenant_id=1,
            adjustment_type="credit",
            reason="Missing expense",
            amount=Decimal("1.00"),
            target_period_start=date(2026, 4, 1),
            target_period_end=date(2026, 4, 30),
            linked_expense_id=999,
        )
    with pytest.raises(ValueError, match="linked payment does not belong to linked invoice"):
        create_gst_adjustment(
            db,
            tenant_id=1,
            adjustment_type="debit",
            reason="Mismatched invoice",
            amount=Decimal("1.00"),
            target_period_start=date(2026, 4, 1),
            target_period_end=date(2026, 4, 30),
            linked_invoice_id=other_invoice.id,
            linked_payment_id=payment.id,
        )

    valid = create_gst_adjustment(
        db,
        tenant_id=1,
        adjustment_type="credit",
        reason="Valid links",
        amount=Decimal("1.00"),
        target_period_start=date(2026, 4, 1),
        target_period_end=date(2026, 4, 30),
        linked_invoice_id=payment.invoice_id,
        linked_payment_id=payment.id,
        linked_expense_id=expense.id,
    )
    assert valid.id is not None


def test_prepared_snapshot_is_not_changed_by_later_source_edits() -> None:
    db = make_db()
    payment, expense = seed_cash_basis_activity(db)
    gst_return = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)
    locked_return = lock_gst_return(db, gst_return.id, tenant_id=1)

    payment.amount = Decimal("1150.00")
    expense.amount_gross = Decimal("460.00")
    expense.amount_net = Decimal("400.00")
    expense.gst_amount = Decimal("60.00")
    db.commit()
    db.refresh(locked_return)

    rows = db.query(GstReturnSnapshotRow).filter_by(gst_return_id=locked_return.id).order_by(GstReturnSnapshotRow.id).all()
    assert locked_return.status == "locked"
    assert locked_return.total_sales_income == Decimal("575.00")
    assert locked_return.gst_output == Decimal("75.00")
    assert locked_return.total_purchases_expenses == Decimal("230.00")
    assert locked_return.gst_input == Decimal("30.00")
    assert [row.gross_amount for row in rows] == [Decimal("575.00"), Decimal("230.00")]
    assert [row.gst_amount for row in rows] == [Decimal("75.00"), Decimal("30.00")]


def test_duplicate_prepare_for_same_period_reuses_unfiled_return() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    first = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)
    second = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    assert second.id == first.id
    assert second.status == "prepared"
    assert db.query(GstReturn).count() == 1


@pytest.mark.parametrize("status", ["prepared", "locked"])
def test_prepare_reuses_existing_unfiled_return_for_same_period(status: str) -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    existing = GstReturn(tenant_id=1, period_start=date(2026, 4, 1), period_end=date(2026, 4, 30), status=status)
    db.add(existing)
    db.commit()

    prepared = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    assert prepared.id == existing.id
    assert prepared.status == "prepared"


def test_prepare_rejects_existing_filed_return_for_same_period() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    db.add(GstReturn(tenant_id=1, period_start=date(2026, 4, 1), period_end=date(2026, 4, 30), status="filed"))
    db.commit()

    with pytest.raises(ValueError, match="Filed GST returns cannot be updated"):
        prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)


def test_database_rejects_duplicate_gst_return_periods() -> None:
    db = make_db()
    db.add(GstReturn(tenant_id=1, period_start=date(2026, 4, 1), period_end=date(2026, 4, 30), status="draft"))
    db.commit()
    db.add(GstReturn(period_start=date(2026, 4, 1), period_end=date(2026, 4, 30), status="prepared"))

    with pytest.raises(IntegrityError):
        db.commit()


def test_lock_and_file_transitions_set_timestamps_without_recalculation() -> None:
    db = make_db()
    payment, _expense = seed_cash_basis_activity(db)
    gst_return = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    locked_return = lock_gst_return(db, gst_return.id, tenant_id=1)
    payment.amount = Decimal("1150.00")
    db.commit()
    filed_return = file_gst_return(db, locked_return.id, tenant_id=1)

    rows = db.query(GstReturnSnapshotRow).filter_by(gst_return_id=filed_return.id).order_by(GstReturnSnapshotRow.id).all()
    assert filed_return.status == "filed"
    assert filed_return.locked_at is not None
    assert filed_return.filed_at is not None
    assert filed_return.total_sales_income == Decimal("575.00")
    assert filed_return.gst_output == Decimal("75.00")
    assert rows[0].gross_amount == Decimal("575.00")
    assert rows[0].gst_amount == Decimal("75.00")


def make_finance_client(db):
    warnings.filterwarnings("ignore", message="Using `httpx` with `starlette.testclient` is deprecated.*")
    from fastapi.testclient import TestClient

    os.environ["JWT_SECRET"] = "test-secret-value-with-at-least-32-chars"
    jose = types.ModuleType("jose")
    jose.JWTError = Exception
    jose.jwt = types.SimpleNamespace(
        decode=lambda *args, **kwargs: {},
        encode=lambda *args, **kwargs: "token",
    )
    sys.modules.setdefault("jose", jose)

    from app.api.finance import router
    from app.core.database import get_db
    from app.core.security import get_current_user

    app = FastAPI()
    app.include_router(router)

    def override_db():
        try:
            yield db
        finally:
            pass

    async def override_user():
        return type("U", (), {"id": 1, "username": "tester", "tenant_id": 1, "role": "admin"})()

    app.dependency_overrides[get_db] = override_db
    app.dependency_overrides[get_current_user] = override_user
    return TestClient(app)


def test_gst_return_endpoints_map_duplicate_and_invalid_status_to_409_and_missing_to_404() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    client = make_finance_client(db)

    first_prepare = client.post(
        "/api/finance/gst-return/prepare",
        json={"period_start": "2026-04-01", "period_end": "2026-04-30"},
    )
    assert first_prepare.status_code == 200
    return_id = first_prepare.json()["id"]

    duplicate_prepare = client.post(
        "/api/finance/gst-return/prepare",
        json={"period_start": "2026-04-01", "period_end": "2026-04-30"},
    )
    assert duplicate_prepare.status_code == 200
    assert duplicate_prepare.json()["id"] == return_id

    locked = client.post(f"/api/finance/gst-return/{return_id}/lock")
    assert locked.status_code == 200
    second_lock = client.post(f"/api/finance/gst-return/{return_id}/lock")
    assert second_lock.status_code == 409

    missing = client.get("/api/finance/gst-return/999")
    assert missing.status_code == 404

    bad_period = client.post(
        "/api/finance/gst-return/prepare",
        json={"period_start": "2026-05-01", "period_end": "2026-04-30"},
    )
    assert bad_period.status_code == 400


def test_gst_return_draft_save_and_delete_endpoints() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    client = make_finance_client(db)

    saved = client.post(
        "/api/finance/gst-return/drafts",
        json={"period_start": "2026-04-01", "period_end": "2026-04-30"},
    )
    assert saved.status_code == 200
    assert saved.json()["status"] == "draft"

    prepared = client.post(
        "/api/finance/gst-return/prepare",
        json={"period_start": "2026-04-01", "period_end": "2026-04-30"},
    )
    assert prepared.status_code == 200
    assert prepared.json()["id"] == saved.json()["id"]
    assert prepared.json()["status"] == "prepared"

    deleted = client.delete(f"/api/finance/gst-return/{prepared.json()['id']}")
    missing = client.delete("/api/finance/gst-return/999")
    assert deleted.status_code == 200
    assert deleted.json()["status"] == "prepared"
    assert missing.status_code == 404


def test_gst_return_file_endpoint_rejects_second_file_attempt_with_409() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    client = make_finance_client(db)

    prepared = client.post(
        "/api/finance/gst-return/prepare",
        json={"period_start": "2026-04-01", "period_end": "2026-04-30"},
    )
    assert prepared.status_code == 200
    return_id = prepared.json()["id"]

    first_file = client.post(f"/api/finance/gst-return/{return_id}/file")
    assert first_file.status_code == 200

    second_file = client.post(f"/api/finance/gst-return/{return_id}/file")
    assert second_file.status_code == 409


def test_gst_return_list_endpoint_returns_prepared_returns_newest_first() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    client = make_finance_client(db)

    first = prepare_gst_return(db, date(2026, 1, 1), date(2026, 3, 31), tenant_id=1)
    second = prepare_gst_return(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    response = client.get("/api/finance/gst-returns")

    assert response.status_code == 200
    body = response.json()
    assert [item["id"] for item in body["items"]] == [second.id, first.id]
    assert body["total"] == 2
    assert body["items"][0]["period_start"] == "2026-04-01"
    assert body["items"][0]["status"] == "prepared"


def test_gst_adjustment_endpoint_creates_adjustment_and_rejects_invalid_payloads() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    client = make_finance_client(db)

    created = client.post(
        "/api/finance/gst-return/adjustments",
        json={
            "adjustment_type": "debit",
            "reason": "Late correction",
            "amount": "9.99",
            "target_period_start": "2026-04-01",
            "target_period_end": "2026-04-30",
            "source_period_start": "2026-03-01",
            "source_period_end": "2026-03-31",
        },
    )

    assert created.status_code == 200
    body = created.json()
    assert body["adjustment_type"] == "debit"
    assert body["reason"] == "Late correction"
    assert body["amount"] == 9.99
    assert body["target_period_start"] == "2026-04-01"
    assert db.query(GstAdjustment).count() == 1

    bad_amount = client.post(
        "/api/finance/gst-return/adjustments",
        json={
            "adjustment_type": "debit",
            "reason": "Bad amount",
            "amount": "0.00",
            "target_period_start": "2026-04-01",
            "target_period_end": "2026-04-30",
        },
    )
    bad_type = client.post(
        "/api/finance/gst-return/adjustments",
        json={
            "adjustment_type": "output",
            "reason": "Bad type",
            "amount": "1.00",
            "target_period_start": "2026-04-01",
            "target_period_end": "2026-04-30",
        },
    )

    assert bad_amount.status_code == 400
    assert bad_type.status_code == 400


def test_gst_adjustment_delete_endpoint_removes_draft_adjustment() -> None:
    db = make_db()
    seed_cash_basis_activity(db)
    client = make_finance_client(db)
    adjustment = create_gst_adjustment(
        db,
        tenant_id=1,
        adjustment_type="credit",
        reason="Delete through API",
        amount=Decimal("4.50"),
        target_period_start=date(2026, 4, 1),
        target_period_end=date(2026, 4, 30),
    )

    deleted = client.delete(f"/api/finance/gst-return/adjustments/{adjustment.id}")
    missing = client.delete("/api/finance/gst-return/adjustments/999")

    assert deleted.status_code == 200
    assert deleted.json()["id"] == adjustment.id
    assert db.query(GstAdjustment).count() == 0
    assert missing.status_code == 404


def test_gst_adjustment_endpoint_validates_reason_source_period_and_linked_sources() -> None:
    db = make_db()
    payment, _expense = seed_cash_basis_activity(db)
    other_customer = Customer(tenant_id=1, name="API Other Customer")
    db.add(other_customer)
    db.flush()
    other_invoice = Invoice(
        tenant_id=1,
        invoice_number="INV-API-OTHER",
        customer_id=other_customer.id,
        invoice_date=date(2026, 4, 5),
        total_amount=Decimal("10.00"),
        status=InvoiceStatus.sent,
    )
    db.add(other_invoice)
    db.commit()
    client = make_finance_client(db)

    base_payload = {
        "adjustment_type": "debit",
        "reason": "Linked correction",
        "amount": "3.00",
        "target_period_start": "2026-04-01",
        "target_period_end": "2026-04-30",
    }

    blank_reason = client.post(
        "/api/finance/gst-return/adjustments",
        json={**base_payload, "reason": "   "},
    )
    partial_source = client.post(
        "/api/finance/gst-return/adjustments",
        json={**base_payload, "source_period_start": "2026-03-01"},
    )
    reversed_source = client.post(
        "/api/finance/gst-return/adjustments",
        json={
            **base_payload,
            "source_period_start": "2026-03-31",
            "source_period_end": "2026-03-01",
        },
    )
    missing_invoice = client.post(
        "/api/finance/gst-return/adjustments",
        json={**base_payload, "linked_invoice_id": 999},
    )
    mismatched_payment = client.post(
        "/api/finance/gst-return/adjustments",
        json={
            **base_payload,
            "linked_invoice_id": other_invoice.id,
            "linked_payment_id": payment.id,
        },
    )

    assert blank_reason.status_code == 400
    assert blank_reason.json()["detail"] == "reason is required"
    assert partial_source.status_code == 400
    assert partial_source.json()["detail"] == "source_period_start and source_period_end must be provided together"
    assert reversed_source.status_code == 400
    assert reversed_source.json()["detail"] == "source_period_end must be on or after source_period_start"
    assert missing_invoice.status_code == 404
    assert missing_invoice.json()["detail"] == "linked invoice not found"
    assert mismatched_payment.status_code == 400
    assert mismatched_payment.json()["detail"] == "linked payment does not belong to linked invoice"
