"""发票提取编排模块单元测试

覆盖范围：
- extract_invoices：空目录、提取失败、正常流程、分类结果、申请单分离
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from src.doc.extractor import extract_invoices

# 字段键名（与源码中的字符串字面量保持一致）
K_INVOICE_TYPE = "invoice_type"
K_INVOICE_NUMBER = "invoice_number"
K_TOTAL_AMOUNT = "total_amount"
K_ITEM_NAME = "item_name"
K_PERSON_NAME = "person_name"
K_CARD_DATE = "card_date"
K_CARD_NO = "card_no"
K_CARD_AMOUNT = "card_amount"
K_MATCHED_INVOICES = "_matched_invoices"
K_RELATIVE_INVOICE_COUNT = "relative_invoice_count"
K_INVOICE_DETAIL = "invoice_detail"
K_REMARK = "remark"

# 发票类型
INVOICE_TYPE_TRAIN = "train"
INVOICE_TYPE_HOTEL = "hotel"
INVOICE_TYPE_GENERAL = "general"
INVOICE_TYPE_PAYMENT = "payment"
DOCUMENT_TYPE_APPLICATION = "application"

# ------------------------------------------------------------------
# Fixture helpers
# ------------------------------------------------------------------


def _make_invoice(number: str, amount: float, inv_type: str = INVOICE_TYPE_GENERAL) -> dict[str, Any]:
    inv: dict[str, Any] = {
        K_INVOICE_NUMBER: number,
        K_INVOICE_TYPE: inv_type,
        K_TOTAL_AMOUNT: str(amount),
    }
    if inv_type == INVOICE_TYPE_TRAIN:
        inv[K_PERSON_NAME] = f"person{number}"
    elif inv_type == INVOICE_TYPE_GENERAL:
        inv[K_ITEM_NAME] = f"item{number}"
    return inv


def _make_application() -> dict[str, Any]:
    return {
        K_INVOICE_TYPE: DOCUMENT_TYPE_APPLICATION,
        "applicant": "张三",
    }


def _make_card(amount: float) -> dict[str, Any]:
    return {
        K_INVOICE_TYPE: INVOICE_TYPE_PAYMENT,
        K_CARD_DATE: "2026-01-01",
        K_CARD_NO: "6228480000000000",
        K_CARD_AMOUNT: f"{amount:.2f}",
    }


# ------------------------------------------------------------------
# extract_invoices
# ------------------------------------------------------------------


class TestExtractInvoices:
    """extract_invoices 编排函数"""

    def test_empty_directory(self, tmp_path: Path, monkeypatch):
        monkeypatch.setattr("src.doc.extractor._find_all_files", lambda d: [])

        records, apps, groups = extract_invoices(str(tmp_path))
        assert records == []
        assert apps == []
        assert groups == {"travel": [], "general": [], "application": []}

    def test_extraction_fails(self, tmp_path: Path, monkeypatch):
        pdf = tmp_path / "broken.pdf"
        pdf.touch()

        monkeypatch.setattr("src.doc.extractor._find_all_files", lambda d: [pdf])
        monkeypatch.setattr("src.doc.extractor._extract_document", lambda p, c: None)

        records, apps, groups = extract_invoices(str(tmp_path))
        assert records == []
        assert apps == []
        assert groups == {"travel": [], "general": [], "application": []}

    def test_normal_flow_general_invoices(self, tmp_path: Path, monkeypatch):
        pdf1 = tmp_path / "inv1.pdf"
        pdf2 = tmp_path / "inv2.pdf"
        pdf1.touch()
        pdf2.touch()

        inv1 = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)
        inv2 = _make_invoice("INV002", 200.0, INVOICE_TYPE_GENERAL)

        monkeypatch.setattr("src.doc.extractor._find_all_files", lambda d: [pdf1, pdf2])

        call_index = [0]

        def fake_extract(path, cache_dir):
            idx = call_index[0]
            call_index[0] += 1
            return inv1 if idx == 0 else inv2

        monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)

        def fake_match(invoices, cards):
            return [
                {
                    K_CARD_DATE: "2026-01-01",
                    K_CARD_NO: "6228480000000000",
                    K_CARD_AMOUNT: "500.00",
                    K_RELATIVE_INVOICE_COUNT: "2",
                    K_INVOICE_DETAIL: "",
                    K_REMARK: "",
                    K_MATCHED_INVOICES: [inv1, inv2],
                }
            ]

        monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)

        records, apps, groups = extract_invoices(str(tmp_path))

        assert len(records) == 1
        assert records[0][K_RELATIVE_INVOICE_COUNT] == "2"
        assert len(groups["travel"]) == 0
        assert len(groups["general"]) == 2

    def test_normal_flow_mixed_invoices(self, tmp_path: Path, monkeypatch):
        pdf1 = tmp_path / "train.pdf"
        pdf2 = tmp_path / "hotel.pdf"
        pdf3 = tmp_path / "general.pdf"
        pdf1.touch()
        pdf2.touch()
        pdf3.touch()

        inv_train = _make_invoice("TRAIN001", 500.0, INVOICE_TYPE_TRAIN)
        inv_hotel = _make_invoice("HOTEL001", 800.0, INVOICE_TYPE_HOTEL)
        inv_general = _make_invoice("GEN001", 150.0, INVOICE_TYPE_GENERAL)

        monkeypatch.setattr(
            "src.doc.extractor._find_all_files",
            lambda d: [pdf1, pdf2, pdf3],
        )

        invoices_list = [inv_train, inv_hotel, inv_general]
        call_index = [0]

        def fake_extract(path, cache_dir):
            idx = call_index[0]
            call_index[0] += 1
            return invoices_list[idx]

        monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)

        def fake_match(invoices, cards):
            return [
                {
                    K_CARD_DATE: "2026-01-01",
                    K_CARD_NO: "6228480000000000",
                    K_CARD_AMOUNT: "500.00",
                    K_RELATIVE_INVOICE_COUNT: "1",
                    K_INVOICE_DETAIL: "",
                    K_REMARK: "",
                    K_MATCHED_INVOICES: [inv_train],
                },
                {
                    K_CARD_DATE: "2026-01-02",
                    K_CARD_NO: "6228480000000000",
                    K_CARD_AMOUNT: "800.00",
                    K_RELATIVE_INVOICE_COUNT: "1",
                    K_INVOICE_DETAIL: "",
                    K_REMARK: "",
                    K_MATCHED_INVOICES: [inv_hotel],
                },
                {
                    K_CARD_DATE: "",
                    K_CARD_NO: "",
                    K_CARD_AMOUNT: "",
                    K_RELATIVE_INVOICE_COUNT: "1",
                    K_INVOICE_DETAIL: "",
                    K_REMARK: "unmatched",
                    K_MATCHED_INVOICES: [inv_general],
                },
            ]

        monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)

        records, apps, groups = extract_invoices(str(tmp_path))

        assert len(records) == 3
        assert len(groups["travel"]) == 2
        assert len(groups["general"]) == 1

        travel_numbers = {inv[K_INVOICE_NUMBER] for inv in groups["travel"]}
        assert "TRAIN001" in travel_numbers
        assert "HOTEL001" in travel_numbers
        assert groups["general"][0][K_INVOICE_NUMBER] == "GEN001"

    def test_application_documents_separated(self, tmp_path: Path, monkeypatch):
        pdf1 = tmp_path / "invoice.pdf"
        pdf2 = tmp_path / "application.pdf"
        pdf1.touch()
        pdf2.touch()

        inv = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)
        app = _make_application()

        monkeypatch.setattr(
            "src.doc.extractor._find_all_files",
            lambda d: [pdf1, pdf2],
        )

        results = [inv, app]
        call_index = [0]

        def fake_extract(path, cache_dir):
            idx = call_index[0]
            call_index[0] += 1
            return results[idx]

        monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)

        def fake_match(invoices, cards):
            return [
                {
                    K_CARD_DATE: "2026-01-01",
                    K_CARD_NO: "6228480000000000",
                    K_CARD_AMOUNT: "300.00",
                    K_RELATIVE_INVOICE_COUNT: "1",
                    K_INVOICE_DETAIL: "",
                    K_REMARK: "",
                    K_MATCHED_INVOICES: [inv],
                }
            ]

        monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)

        records, apps, groups = extract_invoices(str(tmp_path))

        assert len(apps) == 1
        assert apps[0]["applicant"] == "张三"
        assert len(groups["application"]) == 1

    def test_payment_records_included(self, tmp_path: Path, monkeypatch):
        pdf1 = tmp_path / "invoice.pdf"
        pdf2 = tmp_path / "card.png"
        pdf1.touch()
        pdf2.touch()

        inv = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)
        card = _make_card(300.0)

        monkeypatch.setattr(
            "src.doc.extractor._find_all_files",
            lambda d: [pdf1, pdf2],
        )

        results = [inv, card]
        call_index = [0]

        def fake_extract(path, cache_dir):
            idx = call_index[0]
            call_index[0] += 1
            return results[idx]

        monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)

        def fake_match(invoices, cards):
            return [
                {
                    K_CARD_DATE: "2026-01-01",
                    K_CARD_NO: "6228480000000000",
                    K_CARD_AMOUNT: "300.00",
                    K_RELATIVE_INVOICE_COUNT: "1",
                    K_INVOICE_DETAIL: "",
                    K_REMARK: "",
                    K_MATCHED_INVOICES: [inv],
                }
            ]

        monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)

        records, apps, groups = extract_invoices(str(tmp_path))

        assert len(records) == 1
        assert len(groups["general"]) == 1

    def test_partial_extraction_failure(self, tmp_path: Path, monkeypatch):
        pdf1 = tmp_path / "good.pdf"
        pdf2 = tmp_path / "bad.pdf"
        pdf1.touch()
        pdf2.touch()

        inv = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)

        monkeypatch.setattr(
            "src.doc.extractor._find_all_files",
            lambda d: [pdf1, pdf2],
        )

        results = [inv, None]
        call_index = [0]

        def fake_extract(path, cache_dir):
            idx = call_index[0]
            call_index[0] += 1
            return results[idx]

        monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)

        def fake_match(invoices, cards):
            return [
                {
                    K_CARD_DATE: "2026-01-01",
                    K_CARD_NO: "6228480000000000",
                    K_CARD_AMOUNT: "300.00",
                    K_RELATIVE_INVOICE_COUNT: "1",
                    K_INVOICE_DETAIL: "",
                    K_REMARK: "",
                    K_MATCHED_INVOICES: [inv],
                }
            ]

        monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)

        records, apps, groups = extract_invoices(str(tmp_path))

        assert len(records) == 1
        assert len(groups["general"]) == 1
