import unittest
import os
import sys
import types
import importlib
from decimal import Decimal
from pathlib import Path
from tempfile import TemporaryDirectory

from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker


ROOT = Path(__file__).resolve().parents[1]
os.environ.setdefault("JWT_SECRET", "test-secret-value-with-enough-length-12345")
os.environ.setdefault("DB_NAME", str(ROOT / "test_expense_api_bootstrap.db"))
sys.path.insert(0, str(ROOT))
if "dotenv" not in sys.modules:
    dotenv_stub = types.ModuleType("dotenv")
    dotenv_stub.load_dotenv = lambda *args, **kwargs: None
    sys.modules["dotenv"] = dotenv_stub
if "jose" not in sys.modules:
    jose_stub = types.ModuleType("jose")
    jwt_stub = types.SimpleNamespace(
        encode=lambda *args, **kwargs: "test-token",
        decode=lambda *args, **kwargs: {},
    )
    jose_stub.JWTError = Exception
    jose_stub.jwt = jwt_stub
    sys.modules["jose"] = jose_stub
if "app.services.receipt_service" not in sys.modules:
    receipt_service_stub = types.ModuleType("app.services.receipt_service")
    receipt_service_stub.UPLOAD_ROOT = ROOT / "uploads"
    receipt_service_stub.normalize_receipt_upload = lambda *args, **kwargs: {}
    receipt_service_stub.run_ocr_on_pages = lambda *args, **kwargs: {}
    receipt_service_stub.snapshot_json = lambda value: "{}"
    receipt_service_stub.rename_archive_pages = lambda paths, *args, **kwargs: paths
    receipt_service_stub.process_receipt_upload = None  # ponytail: stub placeholder, real func loaded by tests that need it
    sys.modules["app.services.receipt_service"] = receipt_service_stub
if "python_multipart" not in sys.modules:
    multipart_stub = types.ModuleType("python_multipart")
    multipart_stub.__version__ = "0.0.13"
    sys.modules["python_multipart"] = multipart_stub


def source(path: str) -> str:
    return (ROOT / path).read_text(encoding="utf-8")


class ExpenseReceiptApiTests(unittest.TestCase):
    def make_client(self):
        from fastapi import FastAPI

        from app.core.database import get_db
        from app.core.models import Base, Tenant
        from app.core.security import get_current_user
        from app.api.expenses import router

        temp_dir = TemporaryDirectory()
        db_path = Path(temp_dir.name) / "expenses.db"
        engine = create_engine(
            f"sqlite:///{db_path}",
            connect_args={"check_same_thread": False},
        )
        TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
        Base.metadata.create_all(bind=engine)
        _seed_db = TestingSessionLocal()
        _seed_db.add(Tenant(id=1, company_code="test", name="Test"))
        _seed_db.commit()
        _seed_db.close()

        def override_get_db():
            db = TestingSessionLocal()
            try:
                yield db
            finally:
                db.close()

        async def override_current_user():
            from app.core.models import UserRole
            return type("U", (), {"id": 1, "username": "tester", "tenant_id": 1, "role": UserRole.admin})()

        app = FastAPI()
        app.include_router(router)
        app.dependency_overrides[get_db] = override_get_db
        app.dependency_overrides[get_current_user] = override_current_user
        self.addCleanup(app.dependency_overrides.clear)
        self.addCleanup(temp_dir.cleanup)
        self.addCleanup(engine.dispose)
        return TestClient(app)

    def assertMoney(self, actual, expected: str) -> None:
        self.assertEqual(Decimal(str(actual)).quantize(Decimal("0.01")), Decimal(expected))

    def base_expense_payload(self, **overrides):
        payload = {
            "expense_date": "2026-05-31",
            "vendor_name": "Stationery Co",
            "category": "office",
            "amount_gross": 230,
            "currency": "NZD",
            "gst_claimable": True,
        }
        payload.update(overrides)
        return payload

    def create_expense(self, client: TestClient, **overrides):
        response = client.post("/api/expenses", json=self.base_expense_payload(**overrides))
        self.assertEqual(response.status_code, 201, response.text)
        return response.json()

    def assert_bad_expense_payload(self, client: TestClient, **overrides) -> None:
        response = client.post("/api/expenses", json=self.base_expense_payload(**overrides))
        self.assertEqual(response.status_code, 400, response.text)

    def test_expense_route_module_and_tax_schema_import(self) -> None:
        expenses_module = importlib.import_module("app.api.expenses")

        self.assertEqual(expenses_module.router.prefix, "/api/expenses")
        for field_name in [
            "tax_mode",
            "custom_tax_rate",
            "gst_amount_overridden",
            "gst_amount_override",
        ]:
            self.assertIn(field_name, expenses_module.ExpenseCreate.model_fields)
            self.assertIn(field_name, expenses_module.ExpenseUpdate.model_fields)
            self.assertIn(field_name, expenses_module.ExpenseResponse.model_fields)

    def test_expense_create_calculates_default_gst_15_from_gross(self) -> None:
        client = self.make_client()

        expense = self.create_expense(client, tax_mode="gst_15")

        self.assertEqual(expense["tax_mode"], "gst_15")
        self.assertIsNone(expense["custom_tax_rate"])
        self.assertFalse(expense["gst_amount_overridden"])
        self.assertIsNone(expense["gst_amount_override"])
        self.assertMoney(expense["amount_gross"], "230.00")
        self.assertMoney(expense["gst_amount"], "30.00")
        self.assertMoney(expense["amount_net"], "200.00")

    def test_expense_create_zero_rated_and_no_gst_store_zero_gst(self) -> None:
        client = self.make_client()

        zero_rated = self.create_expense(client, tax_mode="zero_rated")
        no_gst = self.create_expense(client, tax_mode="no_gst")

        for expense, mode in [(zero_rated, "zero_rated"), (no_gst, "no_gst")]:
            self.assertEqual(expense["tax_mode"], mode)
            self.assertMoney(expense["amount_gross"], "230.00")
            self.assertMoney(expense["gst_amount"], "0.00")
            self.assertMoney(expense["amount_net"], "230.00")

    def test_expense_create_normalizes_custom_rate_percent_and_ratio(self) -> None:
        client = self.make_client()

        percent = self.create_expense(client, tax_mode="custom_rate", custom_tax_rate=10)
        ratio = self.create_expense(client, tax_mode="custom_rate", custom_tax_rate=0.2, amount_gross=120)
        negative = self.create_expense(client, tax_mode="custom_rate", custom_tax_rate=-15)

        self.assertMoney(percent["custom_tax_rate"], "0.10")
        self.assertMoney(percent["gst_amount"], "20.91")
        self.assertMoney(percent["amount_net"], "209.09")
        self.assertMoney(ratio["custom_tax_rate"], "0.20")
        self.assertMoney(ratio["gst_amount"], "20.00")
        self.assertMoney(ratio["amount_net"], "100.00")
        self.assertMoney(negative["custom_tax_rate"], "0.00")
        self.assertMoney(negative["gst_amount"], "0.00")
        self.assertMoney(negative["amount_net"], "230.00")

    def test_expense_create_override_uses_override_gst_amount(self) -> None:
        client = self.make_client()

        expense = self.create_expense(
            client,
            tax_mode="gst_15",
            gst_amount_overridden=True,
            gst_amount_override=42.5,
        )

        self.assertTrue(expense["gst_amount_overridden"])
        self.assertMoney(expense["gst_amount_override"], "42.50")
        self.assertMoney(expense["gst_amount"], "42.50")
        self.assertMoney(expense["amount_net"], "187.50")

    def test_expense_create_rejects_negative_amounts_and_invalid_override(self) -> None:
        client = self.make_client()

        self.assert_bad_expense_payload(client, amount_gross=-1)
        self.assert_bad_expense_payload(client, amount_net=-1, amount_gross=0)
        self.assert_bad_expense_payload(
            client,
            gst_amount_overridden=True,
            gst_amount_override=-1,
        )
        self.assert_bad_expense_payload(
            client,
            amount_gross=50,
            gst_amount_overridden=True,
            gst_amount_override=60,
        )

    def test_expense_zero_rated_and_no_gst_ignore_override_amount(self) -> None:
        client = self.make_client()

        zero_rated = self.create_expense(
            client,
            tax_mode="zero_rated",
            gst_amount_overridden=True,
            gst_amount_override=25,
        )
        no_gst = self.create_expense(
            client,
            tax_mode="no_gst",
            gst_amount_overridden=True,
            gst_amount_override=25,
        )

        for expense, mode in [(zero_rated, "zero_rated"), (no_gst, "no_gst")]:
            self.assertEqual(expense["tax_mode"], mode)
            self.assertTrue(expense["gst_amount_overridden"])
            self.assertMoney(expense["gst_amount_override"], "25.00")
            self.assertMoney(expense["gst_amount"], "0.00")
            self.assertMoney(expense["amount_net"], "230.00")

    def test_expense_update_recalculates_and_saves_tax_fields(self) -> None:
        client = self.make_client()
        created = self.create_expense(client)

        response = client.put(
            f"/api/expenses/{created['id']}",
            json={
                "tax_mode": "custom_rate",
                "custom_tax_rate": 15,
                "amount_gross": 115,
                "gst_amount_overridden": True,
                "gst_amount_override": 12,
            },
        )

        self.assertEqual(response.status_code, 200, response.text)
        expense = response.json()
        self.assertEqual(expense["tax_mode"], "custom_rate")
        self.assertMoney(expense["custom_tax_rate"], "0.15")
        self.assertTrue(expense["gst_amount_overridden"])
        self.assertMoney(expense["gst_amount_override"], "12.00")
        self.assertMoney(expense["gst_amount"], "12.00")
        self.assertMoney(expense["amount_net"], "103.00")

    def test_expense_update_rejects_invalid_override_and_zero_gst_modes_ignore_override(self) -> None:
        client = self.make_client()
        created = self.create_expense(client)

        invalid = client.put(
            f"/api/expenses/{created['id']}",
            json={
                "amount_gross": 50,
                "gst_amount_overridden": True,
                "gst_amount_override": 60,
            },
        )
        self.assertEqual(invalid.status_code, 400, invalid.text)

        response = client.put(
            f"/api/expenses/{created['id']}",
            json={
                "tax_mode": "no_gst",
                "amount_gross": 50,
                "gst_amount_overridden": True,
                "gst_amount_override": 25,
            },
        )
        self.assertEqual(response.status_code, 200, response.text)
        expense = response.json()
        self.assertEqual(expense["tax_mode"], "no_gst")
        self.assertMoney(expense["gst_amount"], "0.00")
        self.assertMoney(expense["amount_net"], "50.00")

    def test_expense_api_exposes_receipt_upload_and_confirm_endpoints(self) -> None:
        api = source("app/api/expenses.py")
        main = source("app/main.py")
        service = source("app/services/receipt_service.py")
        requirements = source("requirements.txt")

        self.assertIn('prefix="/api/expenses"', api)
        self.assertIn('@router.post("/upload-receipt"', api)
        self.assertIn('@router.post("/{expense_id}/confirm"', api)
        self.assertIn('@router.post("/{expense_id}/void"', api)
        self.assertIn('@router.delete("/{expense_id}"', api)
        self.assertIn('expense.status != "draft"', api)
        self.assertIn('@router.get("/receipts/{receipt_id}/preview"', api)
        self.assertIn('@router.get("/receipts/{receipt_id}/original"', api)
        self.assertIn("normalize_receipt_upload", api)
        self.assertIn("extract_receipt_data", api)
        self.assertIn("normalize_receipt_upload", service)
        self.assertIn("run_ocr_on_pages", service)
        self.assertIn("rename_archive_pages", service)
        self.assertIn('format="WEBP"', service)
        self.assertIn("PyMuPDF", requirements)
        self.assertIn("rapidocr-onnxruntime", requirements)
        self.assertIn("app.include_router(expenses_router)", main)

    def test_expense_frontend_has_single_actions_column_and_draft_delete(self) -> None:
        page = source("../frontend/src/pages/ExpensesPage.vue")
        api = source("../frontend/src/api/expenses.js")

        self.assertNotIn("{ key: 'actions', label: '操作' }", page)
        self.assertIn("showDelete", page)
        self.assertIn("deleteExpense", page)
        self.assertIn("删除", page)
        self.assertIn("delete: id => api.delete(`/expenses/${id}`)", api)
        self.assertIn("timeout: 180000", api)

    def test_expense_frontend_exposes_tax_mode_custom_rate_and_override_contract(self) -> None:
        expenses_page = source("../frontend/src/pages/ExpensesPage.vue")
        receipt_page = source("../frontend/src/pages/ReceiptInboxPage.vue")

        for page in (expenses_page, receipt_page):
            self.assertIn("tax_mode", page)
            self.assertIn("custom_tax_rate_percent", page)
            self.assertIn("gst_amount_overridden", page)
            self.assertIn("gst_amount_override", page)
            self.assertIn("15% GST", page)
            self.assertIn("Zero-rated", page)
            self.assertIn("No GST", page)
            self.assertIn("Custom rate", page)
            self.assertIn("Custom rate (%)", page)
            self.assertIn("customRateRatio", page)


if __name__ == "__main__":
    unittest.main()
