import unittest
import os
import sys
from datetime import date
from decimal import Decimal
from pathlib import Path
from unittest.mock import patch

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

ROOT = Path(__file__).resolve().parents[2]
BACKEND_ROOT = ROOT / "backend"
if str(BACKEND_ROOT) not in sys.path:
    sys.path.insert(0, str(BACKEND_ROOT))

# Set env vars before importing app.* so the real modules load without
# hitting their module-level guards (JWT_SECRET check, mysql fallback).
os.environ.setdefault("JWT_SECRET", "test-jwt-secret-value-with-32-chars")
os.environ.setdefault("DB_NAME", "test.db")

from app.api.invoices import (
    InvoiceCreate,
    InvoiceUpdate,
    PaymentResponse,
    _invoice_to_response,
    create_invoice,
    list_invoices,
    update_invoice,
)
from app.core.database import Base
from app.core.models import Customer, Invoice, InvoiceStatus, Tenant
from app.services.invoice_service import record_payment, resolve_tax_rate


def source(path: str) -> str:
    return (ROOT / path).read_text(encoding="utf-8")


class FixedDate(date):
    @classmethod
    def today(cls) -> date:
        return cls(2026, 4, 2)


class InvoiceFrontendCrudTests(unittest.TestCase):
    def make_session(self):
        engine = create_engine("sqlite:///:memory:")
        Base.metadata.create_all(engine)
        db = sessionmaker(bind=engine)()
        db.add(Tenant(id=1, company_code="test", name="Test"))
        db.commit()
        return db

    def make_invoice(self, db, total_amount: Decimal = Decimal("100.00")) -> Invoice:
        customer = Customer(tenant_id=1, name="GST Test Customer", email="gst@example.com")
        db.add(customer)
        db.flush()
        invoice = Invoice(
            tenant_id=1,
            invoice_number="INV-TEST-0001",
            customer_id=customer.id,
            invoice_date=date(2026, 3, 1),
            subtotal=total_amount,
            total_tax=Decimal("0.00"),
            total_amount=total_amount,
            currency="NZD",
            status=InvoiceStatus.sent,
        )
        db.add(invoice)
        db.commit()
        db.refresh(invoice)
        return invoice

    def test_invoice_list_exposes_create_update_delete_controls(self) -> None:
        page = source("frontend/src/pages/InvoicesPage.vue")
        layout = source("frontend/src/components/AppLayout.vue")

        self.assertIn("ConfirmDialog", page)
        self.assertIn("function openAdd", page)
        self.assertIn("function openEdit", page)
        self.assertIn("function confirmDelete", page)
        self.assertIn("const search = ref", page)
        self.assertIn("search: search.value", page)
        self.assertIn('v-model="search"', page)
        self.assertIn('@input="onSearch(search)"', page)
        self.assertNotIn('@input="$event => null"', page)
        self.assertIn("const globalSearch = ref", layout)
        self.assertIn("function submitGlobalSearch", layout)
        self.assertIn("@submit.prevent=\"submitGlobalSearch\"", layout)
        self.assertIn('v-model="globalSearch"', layout)
        self.assertIn("invoicesApi.create", page)
        self.assertIn("invoicesApi.update", page)
        self.assertIn("invoicesApi.delete", page)
        self.assertIn("customersApi.list", page)
        self.assertIn("customersApi.list({ page: 1, page_size: 100 })", page)
        self.assertIn("productsApi.list", page)

    def test_invoice_list_filters_by_search_text(self) -> None:
        db = self.make_session()
        matching_customer = Customer(tenant_id=1, name="Acme Hosting", email="billing@acme.example")
        other_customer = Customer(tenant_id=1, name="Blue Repairs", email="accounts@blue.example")
        db.add_all([matching_customer, other_customer])
        db.flush()
        db.add_all([
            Invoice(
                tenant_id=1,
                invoice_number="INV-ACME-001",
                customer_id=matching_customer.id,
                invoice_date=date(2026, 5, 1),
                subtotal=Decimal("120.00"),
                total_tax=Decimal("18.00"),
                total_amount=Decimal("138.00"),
                currency="NZD",
                status=InvoiceStatus.sent,
            ),
            Invoice(
                tenant_id=1,
                invoice_number="INV-BLUE-001",
                customer_id=other_customer.id,
                invoice_date=date(2026, 5, 2),
                subtotal=Decimal("300.00"),
                total_tax=Decimal("45.00"),
                total_amount=Decimal("345.00"),
                currency="NZD",
                status=InvoiceStatus.sent,
            ),
        ])
        db.commit()

        by_customer = list_invoices(current_user=type("U", (), {"username": "test", "tenant_id": 1})(), page=1, page_size=20, search="acme", db=db)
        by_invoice_number = list_invoices(current_user=type("U", (), {"username": "test", "tenant_id": 1})(), page=1, page_size=20, search="blue-001", db=db)
        by_amount = list_invoices(current_user=type("U", (), {"username": "test", "tenant_id": 1})(), page=1, page_size=20, search="138", db=db)

        self.assertEqual([invoice.invoice_number for invoice in by_customer], ["INV-ACME-001"])
        self.assertEqual([invoice.invoice_number for invoice in by_invoice_number], ["INV-BLUE-001"])
        self.assertEqual([invoice.invoice_number for invoice in by_amount], ["INV-ACME-001"])

    def test_invoice_detail_uses_backend_customer_shape_and_payment_endpoint(self) -> None:
        detail_page = source("frontend/src/pages/InvoiceDetailPage.vue")
        api = source("frontend/src/api/invoices.js")

        self.assertIn("customerName", detail_page)
        self.assertIn("/payments", api)
        self.assertNotIn("/record-payment", api)

    def test_record_payment_preserves_explicit_received_date_for_payments_basis_gst(self) -> None:
        db = self.make_session()
        invoice = self.make_invoice(db)

        payment = record_payment(
            db,
            invoice.id,
            Decimal("40.00"),
            method="bank_transfer",
            reference="BANK-31",
            received_date=date(2026, 3, 31),
            tenant_id=1,
        )

        self.assertEqual(payment.received_date, date(2026, 3, 31))

    def test_record_payment_defaults_received_date_to_today(self) -> None:
        db = self.make_session()
        invoice = self.make_invoice(db)

        with patch("app.services.invoice_service.date", FixedDate):
            payment = record_payment(db, invoice.id, Decimal("25.00"), tenant_id=1)

        self.assertEqual(payment.received_date, date(2026, 4, 2))

    def test_record_payment_keeps_status_balance_and_paid_at_behaviour(self) -> None:
        db = self.make_session()
        invoice = self.make_invoice(db)

        first_payment = record_payment(db, invoice.id, Decimal("40.00"), received_date=date(2026, 3, 31), tenant_id=1)
        db.refresh(invoice)
        partial_response = _invoice_to_response(invoice)

        self.assertEqual(first_payment.received_date, date(2026, 3, 31))
        self.assertEqual(invoice.status, InvoiceStatus.partially_paid)
        self.assertIsNone(invoice.paid_at)
        self.assertEqual(partial_response.total_paid, 40.0)
        self.assertEqual(partial_response.balance_due, 60.0)

        record_payment(db, invoice.id, Decimal("60.00"), received_date=date(2026, 4, 1), tenant_id=1)
        db.refresh(invoice)
        paid_response = _invoice_to_response(invoice)

        self.assertEqual(invoice.status, InvoiceStatus.paid)
        self.assertIsNotNone(invoice.paid_at)
        self.assertEqual(paid_response.total_paid, 100.0)
        self.assertEqual(paid_response.balance_due, 0.0)

    def test_payment_response_serializes_received_date_as_iso_date(self) -> None:
        db = self.make_session()
        invoice = self.make_invoice(db)

        payment = record_payment(db, invoice.id, Decimal("100.00"), received_date=date(2026, 3, 31), tenant_id=1)
        response = PaymentResponse.model_validate(payment, from_attributes=True)

        self.assertEqual(response.received_date, date(2026, 3, 31))
        self.assertEqual(response.model_dump(mode="json")["received_date"], "2026-03-31")

    def test_create_invoice_resolves_and_persists_line_tax_modes(self) -> None:
        db = self.make_session()
        customer = Customer(tenant_id=1, name="Line Tax Customer", email="line-tax@example.com")
        db.add(customer)
        db.commit()
        db.refresh(customer)

        response = create_invoice(
            InvoiceCreate(
                customer_id=customer.id,
                invoice_date=date(2026, 4, 3),
                items=[
                    {
                        "description": "Standard GST",
                        "quantity": 1,
                        "unit_price": 100,
                        "tax_mode": "gst_15",
                    },
                    {
                        "description": "Export sale",
                        "quantity": 1,
                        "unit_price": 200,
                        "tax_mode": "zero_rated",
                    },
                    {
                        "description": "Custom GST",
                        "quantity": 1,
                        "unit_price": 300,
                        "tax_mode": "custom_rate",
                        "custom_tax_rate": 0.075,
                    },
                ],
            ),
            current_user=type("U", (), {"username": "test", "tenant_id": 1})(),
            db=db,
        )

        db_items = sorted(response.items, key=lambda item: item.description or "")
        custom_item = next(item for item in db_items if item.description == "Custom GST")
        standard_item = next(item for item in db_items if item.description == "Standard GST")
        zero_item = next(item for item in db_items if item.description == "Export sale")

        self.assertEqual(standard_item.tax_mode, "gst_15")
        self.assertEqual(standard_item.tax_rate, 0.15)
        self.assertEqual(zero_item.tax_mode, "zero_rated")
        self.assertEqual(zero_item.tax_rate, 0.0)
        self.assertEqual(custom_item.tax_mode, "custom_rate")
        self.assertEqual(custom_item.tax_rate, 0.075)
        self.assertEqual(custom_item.custom_tax_rate, 0.075)
        self.assertEqual(response.subtotal, 600.0)
        self.assertEqual(response.total_tax, 37.5)
        self.assertEqual(response.total_amount, 637.5)

    def test_resolve_tax_rate_accepts_percent_custom_rate_and_preserves_zero_modes(self) -> None:
        self.assertEqual(resolve_tax_rate("custom_rate", Decimal("15")), Decimal("0.15"))
        self.assertEqual(resolve_tax_rate("custom_rate", Decimal("0.125")), Decimal("0.125"))
        self.assertEqual(resolve_tax_rate("custom_rate", Decimal("-3")), Decimal("0"))
        self.assertEqual(resolve_tax_rate("zero_rated", None), Decimal("0"))
        self.assertEqual(resolve_tax_rate("no_gst", None), Decimal("0"))

    def test_update_invoice_resolves_and_persists_line_tax_modes(self) -> None:
        db = self.make_session()
        customer = Customer(tenant_id=1, name="Update Tax Customer", email="update-tax@example.com")
        db.add(customer)
        db.commit()
        db.refresh(customer)

        draft = create_invoice(
            InvoiceCreate(
                customer_id=customer.id,
                invoice_date=date(2026, 4, 4),
                items=[
                    {
                        "description": "Original",
                        "quantity": 1,
                        "unit_price": 100,
                        "tax_mode": "gst_15",
                    },
                ],
            ),
            current_user=type("U", (), {"username": "test", "tenant_id": 1})(),
            db=db,
        )

        response = update_invoice(
            draft.id,
            InvoiceUpdate(
                items=[
                    {
                        "description": "No GST service",
                        "quantity": 1,
                        "unit_price": 150,
                        "tax_mode": "no_gst",
                    },
                    {
                        "description": "Percent custom",
                        "quantity": 1,
                        "unit_price": 200,
                        "tax_mode": "custom_rate",
                        "custom_tax_rate": 12.5,
                    },
                ],
            ),
            current_user=type("U", (), {"username": "test", "tenant_id": 1})(),
            db=db,
        )

        no_gst_item = next(item for item in response.items if item.description == "No GST service")
        custom_item = next(item for item in response.items if item.description == "Percent custom")

        self.assertEqual(no_gst_item.tax_mode, "no_gst")
        self.assertEqual(no_gst_item.tax_rate, 0.0)
        self.assertIsNone(no_gst_item.custom_tax_rate)
        self.assertEqual(custom_item.tax_mode, "custom_rate")
        self.assertEqual(custom_item.custom_tax_rate, 0.125)
        self.assertEqual(custom_item.tax_rate, 0.125)
        self.assertEqual(response.total_tax, 25.0)
        self.assertEqual(response.total_amount, 375.0)

    def test_invoice_frontend_contract_supports_line_tax_modes(self) -> None:
        page = source("frontend/src/pages/InvoicesPage.vue")

        self.assertIn("tax_mode: 'gst_15'", page)
        self.assertIn("custom_tax_rate: null", page)
        self.assertIn('v-model="item.tax_mode"', page)
        self.assertIn('value="zero_rated"', page)
        self.assertIn('value="no_gst"', page)
        self.assertIn('value="custom_rate"', page)
        self.assertIn("v-if=\"item.tax_mode === 'custom_rate'\"", page)
        self.assertIn("function customRatePercent", page)
        self.assertIn("function customRateRatio", page)
        self.assertIn("return Number(rate || 0) * 100", page)
        self.assertIn("return Math.max(Number(percent || 0), 0) / 100", page)
        self.assertIn('v-model.number="item.custom_tax_rate_percent"', page)
        self.assertIn("Custom rate (%)", page)
        self.assertIn("custom_tax_rate: item.tax_mode === 'custom_rate' ? customRateRatio", page)
        self.assertIn("tax_mode: item.tax_mode || 'gst_15'", page)

    def test_invoice_payment_frontend_captures_received_date(self) -> None:
        detail_page = source("frontend/src/pages/InvoiceDetailPage.vue")
        invoices_api = source("backend/app/api/invoices.py")

        self.assertIn("received_date: date | None = None", invoices_api)
        self.assertIn("received_date: date | None", invoices_api)

        self.assertIn("received_date: new Date().toISOString().slice(0, 10)", detail_page)
        self.assertIn('v-model="paymentForm.received_date"', detail_page)
        self.assertIn('type="date"', detail_page)
        self.assertIn("真实收款日期", detail_page)
        self.assertIn("received_date: paymentForm.value.received_date", detail_page)
        self.assertIn("payment.received_date", detail_page)

    def test_invoice_detail_keeps_send_action_available_after_first_send(self) -> None:
        detail_page = source("frontend/src/pages/InvoiceDetailPage.vue")
        invoices_api = source("backend/app/api/invoices.py")

        self.assertIn("status === 'sent' || status === 'partially_paid'", detail_page)
        self.assertIn("fn: send", detail_page)
        self.assertIn("label: '再次发送'", detail_page)
        self.assertIn("const success = ref('')", detail_page)
        self.assertIn("success.value = message", detail_page)
        self.assertIn("付款方式可在“系统设置”中自定义", detail_page)
        self.assertIn("InvoiceStatus.sent", invoices_api)
        self.assertIn("InvoiceStatus.partially_paid", invoices_api)
        self.assertIn("await send_email(", invoices_api)
        self.assertIn("EmailLog(", invoices_api)
        self.assertIn("SMTP not configured", invoices_api)
        self.assertIn('"filename": f"{invoice.invoice_number}.pdf"', invoices_api)
        self.assertIn('filename="{invoice.invoice_number}.pdf"', invoices_api)


if __name__ == "__main__":
    unittest.main()
