import json
from datetime import date, datetime
from decimal import Decimal
import os
import sys
from pathlib import Path

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker


ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
os.environ.setdefault("DB_NAME", ":memory:.db")

from app.services.gst_service import (
    allocate_payment_to_tax_groups,
    build_gst_return_draft,
    calculate_expense_gst,
    calculate_gst_from_gross,
    rate_for_mode,
)
from app.core.database import Base
from app.core.models import Customer, Expense, Invoice, InvoiceItem, InvoiceStatus, Payment, Tenant


SNAPSHOT_ROW_KEYS = {
    "row_type",
    "source_id",
    "source_date",
    "customer_or_vendor",
    "description",
    "gross_amount",
    "net_amount",
    "gst_amount",
    "tax_mode",
    "tax_rate",
    "metadata_json",
}


def test_full_gst_15_gross_calculates_inclusive_gst() -> None:
    assert calculate_gst_from_gross(Decimal("230"), Decimal("0.15")) == Decimal("30.00")


def test_unknown_tax_mode_is_not_assumed_taxable() -> None:
    assert rate_for_mode("unknown") == Decimal("0")
    assert rate_for_mode("future_mode") == Decimal("0")


def test_partial_payment_allocates_mixed_tax_groups_proportionally() -> None:
    rows = allocate_payment_to_tax_groups(
        payment_id=10,
        invoice_id=20,
        received_date=date(2026, 5, 31),
        payment_amount=Decimal("1000"),
        tax_groups=[
            {"tax_mode": "gst_15", "tax_rate": Decimal("0.15"), "gross_amount": Decimal("1150")},
            {"tax_mode": "zero_rated", "tax_rate": Decimal("0"), "gross_amount": Decimal("850")},
        ],
    )

    assert len(rows) == 2
    assert set(rows[0]) == SNAPSHOT_ROW_KEYS
    assert rows[0] == {
        "row_type": "payment",
        "source_id": 10,
        "source_date": date(2026, 5, 31),
        "customer_or_vendor": None,
        "description": "",
        "gross_amount": Decimal("575.00"),
        "net_amount": Decimal("500.00"),
        "gst_amount": Decimal("75.00"),
        "tax_mode": "gst_15",
        "tax_rate": Decimal("0.15"),
        "metadata_json": json.dumps(
            {
                "invoice_id": 20,
                "invoice_gross_amount": "2000.00",
                "payment_amount": "1000.00",
                "allocated_from_tax_group_gross": "1150",
            },
            sort_keys=True,
        ),
    }
    assert rows[1]["gross_amount"] == Decimal("425.00")
    assert rows[1]["gst_amount"] == Decimal("0.00")
    assert rows[1]["net_amount"] == Decimal("425.00")
    assert json.loads(rows[1]["metadata_json"])["invoice_id"] == 20


def test_last_group_absorbs_payment_allocation_rounding_residue() -> None:
    rows = allocate_payment_to_tax_groups(
        payment_id=11,
        invoice_id=21,
        received_date=date(2026, 6, 1),
        payment_amount=Decimal("1.00"),
        tax_groups=[
            {"tax_mode": "gst_15", "tax_rate": Decimal("0.15"), "gross_amount": Decimal("1")},
            {"tax_mode": "zero_rated", "tax_rate": Decimal("0"), "gross_amount": Decimal("1")},
            {"tax_mode": "no_gst", "tax_rate": Decimal("0"), "gross_amount": Decimal("1")},
        ],
    )

    assert [row["gross_amount"] for row in rows] == [
        Decimal("0.33"),
        Decimal("0.33"),
        Decimal("0.34"),
    ]
    assert sum(row["gross_amount"] for row in rows) == Decimal("1.00")


def test_payment_allocation_clamps_payment_to_invoice_gross() -> None:
    rows = allocate_payment_to_tax_groups(
        payment_id=12,
        invoice_id=22,
        received_date=date(2026, 6, 2),
        payment_amount=Decimal("2500"),
        tax_groups=[
            {"tax_mode": "gst_15", "tax_rate": Decimal("0.15"), "gross_amount": Decimal("1150")},
            {"tax_mode": "zero_rated", "tax_rate": Decimal("0"), "gross_amount": Decimal("850")},
        ],
    )

    assert [row["gross_amount"] for row in rows] == [Decimal("1150.00"), Decimal("850.00")]
    assert sum(row["gross_amount"] for row in rows) == Decimal("2000.00")
    assert rows[0]["gst_amount"] == Decimal("150.00")
    assert json.loads(rows[0]["metadata_json"])["payment_amount"] == "2000.00"


def test_custom_rate_accepts_ratio_and_percent_inputs() -> None:
    assert rate_for_mode("custom_rate", Decimal("0.125")) == Decimal("0.125")
    assert rate_for_mode("custom_rate", Decimal("12.5")) == Decimal("0.125")
    assert calculate_gst_from_gross(Decimal("112.50"), rate_for_mode("custom_rate", Decimal("12.5"))) == Decimal(
        "12.50"
    )


def test_payment_allocation_supports_custom_rate_tax_groups() -> None:
    rows = allocate_payment_to_tax_groups(
        payment_id=13,
        invoice_id=23,
        received_date=date(2026, 6, 2),
        payment_amount=Decimal("112.50"),
        tax_groups=[
            {"tax_mode": "custom_rate", "custom_tax_rate": Decimal("12.5"), "gross_amount": Decimal("112.50")},
        ],
    )

    assert rows[0]["gross_amount"] == Decimal("112.50")
    assert rows[0]["tax_rate"] == Decimal("0.125")
    assert rows[0]["gst_amount"] == Decimal("12.50")
    assert rows[0]["net_amount"] == Decimal("100.00")


def test_expense_gst_honors_claimable_tax_modes_and_override_clamps() -> None:
    assert calculate_expense_gst(Decimal("230"), "gst_15", None, True) == Decimal("30.00")
    assert calculate_expense_gst(Decimal("230"), "gst_15", None, False) == Decimal("0.00")
    assert calculate_expense_gst(Decimal("230"), "no_gst", None, True, Decimal("50")) == Decimal("0.00")
    assert calculate_expense_gst(Decimal("230"), "zero_rated", None, True, Decimal("50")) == Decimal("0.00")
    assert calculate_expense_gst(Decimal("230"), "gst_15", None, True, Decimal("999")) == Decimal("230.00")
    assert calculate_expense_gst(Decimal("230"), "gst_15", None, True, Decimal("-1")) == Decimal("0.00")


def test_build_gst_return_draft_uses_payment_received_date_and_confirmed_expenses() -> None:
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    SessionLocal = sessionmaker(bind=engine)
    db = SessionLocal()
    db.add(Tenant(id=1, company_code="test", name="Test"))
    db.commit()

    customer = Customer(tenant_id=1, name="Acme Studio", company_name="Acme Limited")
    db.add(customer)
    db.flush()

    invoice = Invoice(
        tenant_id=1,
        invoice_number="INV-GST-001",
        customer_id=customer.id,
        invoice_date=date(2026, 3, 31),
        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="Design 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"),
        )
    )
    db.add(Payment(tenant_id=1, invoice_id=invoice.id, received_date=date(2026, 4, 2), amount=Decimal("575.00")))
    db.add(Payment(tenant_id=1, invoice_id=invoice.id, received_date=date(2026, 5, 1), amount=Decimal("575.00")))

    legacy_invoice = Invoice(
        tenant_id=1,
        invoice_number="INV-GST-LEGACY",
        customer_id=customer.id,
        invoice_date=date(2026, 3, 15),
        subtotal=Decimal("100.00"),
        total_tax=Decimal("15.00"),
        total_amount=Decimal("115.00"),
        status=InvoiceStatus.sent,
    )
    db.add(legacy_invoice)
    db.flush()
    db.add(
        InvoiceItem(
            tenant_id=1,
            invoice_id=legacy_invoice.id,
            description="Legacy paid work",
            quantity=Decimal("1"),
            unit_price=Decimal("100.00"),
            tax_mode="gst_15",
            tax_rate=Decimal("0.1500"),
            subtotal=Decimal("100.00"),
            tax_amount=Decimal("15.00"),
            total=Decimal("115.00"),
        )
    )
    db.add(
        Payment(
            tenant_id=1,
            invoice_id=legacy_invoice.id,
            paid_at=datetime(2026, 4, 6, 10, 30),
            received_date=None,
            amount=Decimal("115.00"),
        )
    )

    void_invoice = Invoice(
        tenant_id=1,
        invoice_number="INV-GST-VOID",
        customer_id=customer.id,
        invoice_date=date(2026, 4, 1),
        subtotal=Decimal("100.00"),
        total_tax=Decimal("15.00"),
        total_amount=Decimal("115.00"),
        status=InvoiceStatus.void,
    )
    db.add(void_invoice)
    db.flush()
    db.add(
        InvoiceItem(
            tenant_id=1,
            invoice_id=void_invoice.id,
            description="Voided work",
            quantity=Decimal("1"),
            unit_price=Decimal("100.00"),
            tax_mode="gst_15",
            subtotal=Decimal("100.00"),
            tax_amount=Decimal("15.00"),
            total=Decimal("115.00"),
        )
    )
    db.add(Payment(tenant_id=1, invoice_id=void_invoice.id, received_date=date(2026, 4, 3), amount=Decimal("115.00")))

    db.add(
        Expense(
            tenant_id=1,
            expense_date=date(2026, 4, 4),
            vendor_name="Office 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(
            tenant_id=1,
            expense_date=date(2026, 4, 5),
            vendor_name="Draft Vendor",
            category="Supplies",
            amount_net=Decimal("100.00"),
            gst_amount=Decimal("15.00"),
            amount_gross=Decimal("115.00"),
            tax_mode="gst_15",
            gst_claimable=True,
            status="draft",
        )
    )
    db.commit()

    draft = build_gst_return_draft(db, date(2026, 4, 1), date(2026, 4, 30), tenant_id=1)

    assert draft["total_sales_and_income"] == Decimal("690.00")
    assert draft["gst_output"] == Decimal("90.00")
    assert draft["total_purchases_expenses"] == Decimal("230.00")
    assert draft["gst_input"] == Decimal("30.00")
    assert draft["gst_payable"] == Decimal("60.00")
    assert draft["totals"]["total_sales_and_income"] == Decimal("690.00")
    assert draft["totals"]["gst_output"] == Decimal("90.00")
    assert draft["totals"]["debit_adjustments"] == Decimal("0.00")
    assert draft["totals"]["total_gst_collected"] == Decimal("90.00")
    assert draft["totals"]["total_purchases_expenses"] == Decimal("230.00")
    assert draft["totals"]["gst_input"] == Decimal("30.00")
    assert draft["totals"]["credit_adjustments"] == Decimal("0.00")
    assert draft["totals"]["total_gst_purchases"] == Decimal("30.00")
    assert draft["totals"]["gst_payable"] == Decimal("60.00")
    assert len(draft["payment_rows"]) == 2
    assert len(draft["expense_rows"]) == 1
    assert len(draft["rows"]) == 3
    assert [row["row_type"] for row in draft["rows"]] == ["payment", "payment", "expense"]
    assert draft["payment_rows"][0]["source_date"] == date(2026, 4, 2)
    assert draft["payment_rows"][1]["source_date"] == date(2026, 4, 6)
    assert draft["payment_rows"][0]["customer_or_vendor"] == "Acme Limited"
    assert draft["expense_rows"][0]["customer_or_vendor"] == "Office Supplier"
