"""Unit & Integration Tests for Automated Drawing Takeoff & BoQ / WBS Engine.
Specialists: Quynh (QS & BoQ Takeoff), Hung (Site Engineering), Minh (Legal Norms 2026).
"""

from __future__ import annotations

import io
from decimal import Decimal
from pathlib import Path

import fitz  # PyMuPDF
from fastapi.testclient import TestClient

from app.core.postgres.erp_client import ErpDatabaseClient
from app.main import create_app
from app.modules.takeoff.application.drawing_takeoff_service import (
    DrawingTakeoffService,
    get_drawing_takeoff_service,
)

client = TestClient(create_app())
AUTH_HEADERS = {"Authorization": "Bearer dev-test-token"}


def create_sample_drawing_pdf(
    tmp_path: Path, filename: str = "DWG_CONG_BEN_KEM.pdf"
) -> Path:
    """Helper to generate a clean sample engineering CAD blueprint PDF."""
    pdf_path = tmp_path / filename
    pdf_path.parent.mkdir(parents=True, exist_ok=True)
    doc = fitz.open()
    page = doc.new_page(width=842, height=595)  # A4 Landscape blueprint size

    # Draw drawing title box & dimensions
    page.draw_rect(fitz.Rect(20, 20, 822, 575), color=(0, 0.4, 0.8), width=2)
    page.insert_text(
        fitz.Point(40, 60),
        "CÔNG TY TNHH XÂY DỰNG ĐỊNH SƠN - DSCons",
        fontsize=14,
        color=(0, 0.2, 0.6),
    )
    page.insert_text(
        fitz.Point(40, 90),
        "BẢN VẼ CHI TIẾT: CỐNG HỘP BẾN KEM - KÈ ĐA ĐỘ",
        fontsize=16,
        color=(0, 0, 0),
    )
    page.insert_text(
        fitz.Point(40, 120),
        "Thông số thiết kế: Dài L=25.0m, Rộng B=6.5m, Chiều sâu hố móng H=2.2m",
        fontsize=11,
    )
    page.insert_text(
        fitz.Point(40, 140),
        "Bê tông lót đáy: Mác 100 đá 4x6 dày 10cm | Bê tông thân cống: Mác 300 đá 1x2",
        fontsize=11,
    )
    page.insert_text(
        fitz.Point(40, 160),
        "Cốt thép chịu lực: CB300-V D<=18mm | Ván khuôn thép định hình",
        fontsize=11,
    )
    page.insert_text(
        fitz.Point(40, 180),
        "Tỷ lệ: 1:100 | Ngày lập: 24/08/2026",
        fontsize=10,
        color=(0.3, 0.3, 0.3),
    )

    doc.save(str(pdf_path))
    doc.close()
    return pdf_path


def test_render_pdf_to_images(tmp_path: Path):
    """Test PyMuPDF raster rendering of CAD drawing PDF to Base64 image."""
    pdf_file = create_sample_drawing_pdf(tmp_path)
    service = get_drawing_takeoff_service()
    rendered = service.render_pdf_to_images(pdf_file, max_pages=2, dpi=100)

    assert len(rendered) >= 1
    assert "image_base64" in rendered[0]
    assert len(rendered[0]["image_base64"]) > 100
    assert "DSCons" in rendered[0]["text_layer"]
    assert "L=25.0m" in rendered[0]["text_layer"]


def test_analyze_drawing_and_takeoff_service(tmp_path: Path):
    """Test full takeoff processing pipeline including vision/fallback and database persistence."""
    import asyncio

    pdf_file = create_sample_drawing_pdf(tmp_path, "DWG_KE_DADO_M300.pdf")
    service = get_drawing_takeoff_service()

    result = asyncio.run(
        service.process_drawing_file(
            file_path=pdf_file,
            filename="DWG_KE_DADO_M300.pdf",
            drawing_hint="Kè Đa Độ mác 300 cống hộp thủy lợi",
        )
    )

    assert result["status"] == "success"
    takeoff = result["takeoff"]
    items = result["items"]
    metrics = result["metrics"]

    assert takeoff["id"] is not None
    assert len(items) >= 4
    assert metrics["total_estimated_cost_vnd"] > 0
    assert metrics["total_concrete_volume_m3"] > 0

    # Verify decimal precision & state norm codes
    for itm in items:
        assert itm["norm_code"] is not None
        assert isinstance(itm["unit"], str) and len(itm["unit"].strip()) > 0
        assert Decimal(str(itm["quantity"])) >= Decimal("0.0000")

    # Clean up test takeoff
    db_client = ErpDatabaseClient()
    db_client.delete_drawing_takeoff(takeoff["id"])


def test_takeoff_api_crud_flow(tmp_path: Path):
    """Test complete REST API lifecycle for drawing takeoff."""
    pdf_file = create_sample_drawing_pdf(tmp_path, "API_TEST_DRAWING.pdf")

    # 1. Upload & Analyze
    with open(pdf_file, "rb") as f:
        resp = client.post(
            "/v1/takeoff/upload-and-analyze",
            files={"file": ("API_TEST_DRAWING.pdf", f, "application/pdf")},
            data={"drawing_hint": "Bản vẽ móng cống kiểm thử API"},
            headers=AUTH_HEADERS,
        )
    assert resp.status_code == 201, resp.text
    data = resp.json()
    takeoff_id = data["takeoff"]["id"]
    first_item_id = data["items"][0]["id"]

    try:
        # 2. Get Takeoff List
        list_resp = client.get("/v1/takeoff/list", headers=AUTH_HEADERS)
        assert list_resp.status_code == 200
        takeoffs = list_resp.json()["data"]
        assert any(t["id"] == takeoff_id for t in takeoffs)

        # 3. Get Takeoff Detail
        detail_resp = client.get(f"/v1/takeoff/{takeoff_id}", headers=AUTH_HEADERS)
        assert detail_resp.status_code == 200
        assert len(detail_resp.json()["items"]) >= 1

        # 4. Update Takeoff Item
        update_resp = client.put(
            f"/v1/takeoff/items/{first_item_id}",
            json={
                "quantity": 100.5,
                "unit_price_vnd": 50000.0,
                "dimension_formula": "Kích thước thực tế điều chỉnh 100.5 m3",
            },
            headers=AUTH_HEADERS,
        )
        assert update_resp.status_code == 200
        assert float(update_resp.json()["data"]["total_amount_vnd"]) == 100.5 * 50000.0

        # 5. Sync to Project WBS
        erp_db = ErpDatabaseClient()
        import uuid
        from decimal import Decimal
        test_proj = erp_db.create_project(
            {
                "project_code": f"TEST-SYNC-{uuid.uuid4().hex[:6]}",
                "project_name": "Test Project for WBS Sync",
                "contract_value": Decimal("500000000"),
                "status": "planning",
            }
        )
        test_project_id = str(test_proj["id"])
        sync_resp = client.post(
            f"/v1/takeoff/{takeoff_id}/sync-wbs",
            json={"project_id": test_project_id},
            headers=AUTH_HEADERS,
        )
        assert sync_resp.status_code == 200
        # 6. Export to A3 PDF via API
        a3_resp = client.get(
            f"/v1/takeoff/{takeoff_id}/export-a3-pdf?orientation=landscape&color_scheme=print_ready",
            headers=AUTH_HEADERS,
        )
        assert a3_resp.status_code == 200
        assert a3_resp.headers["content-type"] == "application/pdf"
        assert len(a3_resp.content) > 1000
    finally:
        client.delete(f"/v1/takeoff/{takeoff_id}", headers=AUTH_HEADERS)
        if "test_project_id" in locals() and test_project_id:
            with erp_db.get_connection() as conn:
                with conn.cursor() as cur:
                    cur.execute("DELETE FROM erp_project_wbs WHERE project_id = %s;", (test_project_id,))
                    cur.execute("DELETE FROM projects WHERE id = %s;", (test_project_id,))
                conn.commit()


def test_superadmin_delete_and_clear_all_takeoffs(tmp_path: Path):
    """Test single deletion and batch clear-all endpoints for superadmin (scoped to test project)."""
    import uuid
    from decimal import Decimal

    erp_db = ErpDatabaseClient()
    test_proj = erp_db.create_project(
        {
            "project_code": f"DA-TEST-{uuid.uuid4().hex[:6].upper()}",
            "project_name": "Dự án Test Delete Scoped",
            "contract_value": Decimal("1000000000.0000"),
            "status": "planning",
        }
    )
    test_project_id = str(test_proj["id"])
    pdf1 = create_sample_drawing_pdf(tmp_path, "DRAWING_DEL_1.pdf")
    pdf2 = create_sample_drawing_pdf(tmp_path, "DRAWING_DEL_2.pdf")

    try:
        # Upload drawing 1 attached to test_project_id
        with open(pdf1, "rb") as f:
            resp1 = client.post(
                "/v1/takeoff/upload",
                files={"file": ("DRAWING_DEL_1.pdf", f, "application/pdf")},
                data={
                    "drawing_hint": "Bản vẽ 1 cần xóa",
                    "auto_analyze": "false",
                    "project_id": test_project_id,
                },
                headers=AUTH_HEADERS,
            )
        assert resp1.status_code == 201
        id1 = resp1.json()["takeoff"]["id"]

        # Upload drawing 2 attached to test_project_id
        with open(pdf2, "rb") as f:
            resp2 = client.post(
                "/v1/takeoff/upload",
                files={"file": ("DRAWING_DEL_2.pdf", f, "application/pdf")},
                data={
                    "drawing_hint": "Bản vẽ 2 cần xóa",
                    "auto_analyze": "false",
                    "project_id": test_project_id,
                },
                headers=AUTH_HEADERS,
            )
        assert resp2.status_code == 201
        id2 = resp2.json()["takeoff"]["id"]

        # 1. Delete single drawing
        del_single = client.delete(f"/v1/takeoff/{id1}", headers=AUTH_HEADERS)
        assert del_single.status_code == 200
        assert del_single.json()["status"] == "success"

        # Verify id1 is gone
        get_resp = client.get(f"/v1/takeoff/{id1}", headers=AUTH_HEADERS)
        assert get_resp.status_code == 404

        # 2. Test clear-all batch delete endpoint scoped to test_project_id
        clear_resp = client.delete(
            f"/v1/takeoff/clear-all?project_id={test_project_id}", headers=AUTH_HEADERS
        )
        assert clear_resp.status_code == 200
        assert clear_resp.json()["status"] == "success"
        assert clear_resp.json()["deleted_count"] >= 1
    finally:
        erp_db.delete_all_drawing_takeoffs(project_id=test_project_id)
        with erp_db.get_connection() as conn, conn.cursor() as cur:
            cur.execute("DELETE FROM projects WHERE id = %s", (test_project_id,))
            conn.commit()


def test_fast_decoupled_takeoff_upload_and_async_analyze(tmp_path: Path):
    """Test decoupled fast upload (< 500ms) and background / on-demand AI analyze endpoints."""
    pdf_file = create_sample_drawing_pdf(tmp_path, "FAST_UPLOAD_TEST.pdf")

    # 1. Fast Upload without blocking
    with open(pdf_file, "rb") as f:
        resp = client.post(
            "/v1/takeoff/upload",
            files={"file": ("FAST_UPLOAD_TEST.pdf", f, "application/pdf")},
            data={
                "drawing_hint": "Bản vẽ kiểm thử bóc tách bất đồng bộ",
                "auto_analyze": "true",
            },
            headers=AUTH_HEADERS,
        )
    assert resp.status_code == 201, resp.text
    data = resp.json()
    assert data["status"] == "success"
    assert data["is_async"] is True
    takeoff = data["takeoff"]
    takeoff_id = takeoff["id"]
    assert takeoff_id is not None
    assert takeoff["drawing_code"].startswith("DWG-")

    # 2. Trigger on-demand analyze endpoint
    analyze_resp = client.post(
        f"/v1/takeoff/{takeoff_id}/analyze",
        data={"drawing_hint": "Kè Đa Độ mác 300", "run_async": "false"},
        headers=AUTH_HEADERS,
    )
    assert analyze_resp.status_code == 200, analyze_resp.text
    analyze_data = analyze_resp.json()
    assert analyze_data["status"] == "success"
    assert len(analyze_data["items"]) >= 1
    assert analyze_data["metrics"]["total_estimated_cost_vnd"] > 0

    # 3. Clean up
    del_resp = client.delete(f"/v1/takeoff/{takeoff_id}", headers=AUTH_HEADERS)
    assert del_resp.status_code == 200


def test_export_cad_drawing_to_a3_pdf_service(tmp_path: Path):
    """Test A3 PDF export service directly on CAD drawing and verify page dimensions."""
    import pymupdf

    pdf_file = create_sample_drawing_pdf(tmp_path, "TEST_CAD_FOR_A3.pdf")
    output_a3_path = tmp_path / "OUTPUT_SIZE_A3.pdf"

    service = get_drawing_takeoff_service()
    pdf_bytes = service.export_drawing_to_a3_pdf(
        file_path=pdf_file,
        output_pdf_path=output_a3_path,
        orientation="landscape",
        color_scheme="print_ready",
        dpi=300,
    )

    assert len(pdf_bytes) > 1000
    assert output_a3_path.exists()

    # Open with PyMuPDF to verify exact A3 page dimensions
    doc = pymupdf.open(str(output_a3_path))
    assert len(doc) >= 1
    page = doc[0]
    width_mm = page.rect.width * 25.4 / 72.0
    height_mm = page.rect.height * 25.4 / 72.0

    # Verify Landscape A3 dimensions (420mm x 297mm within 2% margin)
    assert 400.0 <= width_mm <= 430.0
    assert 280.0 <= height_mm <= 310.0
    doc.close()


def test_direct_convert_cad_to_a3_pdf_endpoint(tmp_path: Path):
    """Test direct POST upload and convert to A3 PDF endpoint."""
    pdf_file = create_sample_drawing_pdf(tmp_path, "DIRECT_CONVERT.pdf")

    with open(pdf_file, "rb") as f:
        resp = client.post(
            "/v1/takeoff/convert-to-a3-pdf",
            files={"file": ("DIRECT_CONVERT.pdf", f, "application/pdf")},
            data={
                "orientation": "landscape",
                "color_scheme": "print_ready",
                "dpi": 300,
            },
            headers=AUTH_HEADERS,
        )

    assert resp.status_code == 200
    assert resp.headers["content-type"] == "application/pdf"
    assert "A3" in resp.headers.get("X-Paper-Size", "")
    assert len(resp.content) > 1000


def test_quynh_gemini_3_1_pro_high_reasoning(tmp_path: Path):
    """Test that AI Quynh uses Gemini 3.1 Pro High Reasoning model and records correct vision model tag."""
    import asyncio

    pdf_file = create_sample_drawing_pdf(tmp_path, "GEMINI_31_PRO_TEST.pdf")
    service = get_drawing_takeoff_service()

    result = asyncio.run(
        service.process_drawing_file(
            file_path=pdf_file,
            filename="GEMINI_31_PRO_TEST.pdf",
            drawing_hint="Kiểm thử Gemini 3.1 Pro High Reasoning",
        )
    )

    assert result["status"] == "success"
    takeoff = result["takeoff"]
    assert takeoff["ai_vision_model"] == "gemini-3.1-pro-high"

    # Clean up
    db_client = ErpDatabaseClient()
    db_client.delete_drawing_takeoff(takeoff["id"])


def test_quynh_hsmt_auto_discovery_and_confidence(tmp_path: Path):
    """Test AI Quynh QS proactive HSMT auto-discovery and confidence level reporting."""
    import asyncio

    db_client = ErpDatabaseClient()

    # 1. Drawing with HSMT auto-discovery match
    pdf_nvh = create_sample_drawing_pdf(tmp_path, "NVH_DAI_THANG_TEST.pdf")
    service = get_drawing_takeoff_service()

    result_nvh = asyncio.run(
        service.process_drawing_file(
            file_path=pdf_nvh,
            filename="NVH_DAI_THANG_TEST.pdf",
            drawing_hint="Nhà văn hóa thôn Đại Thắng",
        )
    )
    assert result_nvh["status"] == "success"
    takeoff_nvh = result_nvh["takeoff"]
    assert takeoff_nvh["confidence_score"] >= 90
    assert takeoff_nvh["confidence_level"] == "hsmt_verified"
    assert (
        "Đại Thắng" in takeoff_nvh["hsmt_reference_file"]
        or "NVH" in takeoff_nvh["hsmt_reference_file"]
    )
    assert "Thưa sếp" in takeoff_nvh["ai_report_message"]
    assert "98%" in takeoff_nvh["ai_report_message"]

    # 2. Drawing without HSMT (Preliminary geometry)
    pdf_new = create_sample_drawing_pdf(tmp_path, "NEW_UNKNOWN_STRUCTURE.pdf")
    result_new = asyncio.run(
        service.process_drawing_file(
            file_path=pdf_new,
            filename="NEW_UNKNOWN_STRUCTURE.pdf",
            drawing_hint="Công trình kết cấu mới chưa có dự toán",
        )
    )
    assert result_new["status"] == "success"
    takeoff_new = result_new["takeoff"]
    assert takeoff_new["confidence_score"] == 75
    assert takeoff_new["confidence_level"] == "preliminary"
    assert (
        "chưa có hồ sơ mời thầu" in takeoff_new["ai_report_message"].lower()
        or "chưa tìm thấy" in takeoff_new["ai_report_message"].lower()
    )

    # Clean up both test takeoffs
    db_client.delete_drawing_takeoff(takeoff_nvh["id"])
    db_client.delete_drawing_takeoff(takeoff_new["id"])


def test_attach_hsmt_endpoint(tmp_path: Path):
    """Test POST /v1/takeoff/{takeoff_id}/attach-hsmt endpoint upgrades confidence to 99%."""
    pdf_file = create_sample_drawing_pdf(tmp_path, "PRELIMINARY_DRAWING.pdf")
    service = get_drawing_takeoff_service()
    db_client = ErpDatabaseClient()

    # 1. Create a preliminary takeoff
    created = service.create_pending_takeoff(
        file_path=pdf_file,
        filename="PRELIMINARY_DRAWING.pdf",
        drawing_hint="Bản vẽ sơ bộ",
    )
    takeoff_id = created["id"]

    try:
        # 2. Attach mock Excel HSMT
        dummy_excel = io.BytesIO(b"DUMMY EXCEL HSMT BOQ DATA")
        resp = client.post(
            f"/v1/takeoff/{takeoff_id}/attach-hsmt",
            files={
                "file": (
                    "TIEN_LUONG_BOQ_2026.xlsx",
                    dummy_excel,
                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                )
            },
            headers=AUTH_HEADERS,
        )
        assert resp.status_code == 200
        res_data = resp.json()
        assert res_data["status"] == "success"

        # 3. Verify takeoff record is updated with 99% confidence
        updated = db_client.get_drawing_takeoff(takeoff_id)
        assert updated["confidence_score"] == 99
        assert updated["confidence_level"] == "hsmt_verified"
        assert updated["hsmt_reference_file"] == "TIEN_LUONG_BOQ_2026.xlsx"
        assert "TIEN_LUONG_BOQ_2026.xlsx" in updated["ai_report_message"]
    finally:
        # Clean up
        db_client.delete_drawing_takeoff(takeoff_id)


def test_ai_friendly_pdf_structure_extraction(tmp_path):
    """Kiểm thử trích xuất dữ liệu PDF không gian và bảng biểu chuẩn AI-Friendly Markdown."""
    pdf_file = create_sample_drawing_pdf(tmp_path, "TEST_AI_FRIENDLY.pdf")
    service = get_drawing_takeoff_service()

    extracted = service.extract_ai_friendly_pdf_structure(pdf_file, max_pages=5)
    assert "structured_markdown" in extracted
    assert "HỒ SƠ BẢN VẼ KỸ THUẬT" in extracted["structured_markdown"]
    assert "TRANG 1" in extracted["structured_markdown"]
    assert (
        "KHUNG TÊN BẢN VẼ" in extracted["structured_markdown"]
        or "THÔNG SỐ" in extracted["structured_markdown"]
    )


def test_batch_upload_drawings_endpoint(tmp_path):
    """Kiểm thử Endpoint POST /v1/takeoff/upload-batch tải lên đồng thời nhiều bản vẽ."""
    pdf1 = create_sample_drawing_pdf(tmp_path, "BATCH_DRAWING_1.pdf")
    pdf2 = create_sample_drawing_pdf(tmp_path, "BATCH_DRAWING_2.pdf")

    with open(pdf1, "rb") as f1, open(pdf2, "rb") as f2:
        resp = client.post(
            "/v1/takeoff/upload-batch",
            files=[
                ("files", ("BATCH_DRAWING_1.pdf", f1, "application/pdf")),
                ("files", ("BATCH_DRAWING_2.pdf", f2, "application/pdf")),
            ],
            data={"drawing_hint": "Gói thầu hàng loạt batch", "auto_analyze": "false"},
            headers=AUTH_HEADERS,
        )

    assert resp.status_code == 201
    data = resp.json()
    assert data["status"] == "success"
    assert data["count"] == 2
    assert len(data["takeoffs"]) == 2

    # Clean up test takeoffs
    db_client = ErpDatabaseClient()
    for item in data["takeoffs"]:
        db_client.delete_drawing_takeoff(item["id"])


def test_smart_batch_upload_dual_pairing(tmp_path):
    """Kiểm thử tải lên đồng thời 1 file Bản vẽ PDF + 1 file Tiên lượng Excel và ghép cặp kép tự động."""
    pdf_drawing = create_sample_drawing_pdf(tmp_path, "BAN_VE_THI_CONG_CONG_HOP.pdf")
    excel_hsmt = tmp_path / "TIEN_LUONG_DU_TOAN_CONG_HOP.xlsx"
    excel_hsmt.write_bytes(b"PK\x03\x04DUMMY EXCEL HSMT DATA")

    with open(pdf_drawing, "rb") as f_dwg, open(excel_hsmt, "rb") as f_hsmt:
        resp = client.post(
            "/v1/takeoff/upload-batch",
            files=[
                ("files", ("BAN_VE_THI_CONG_CONG_HOP.pdf", f_dwg, "application/pdf")),
                (
                    "files",
                    (
                        "TIEN_LUONG_DU_TOAN_CONG_HOP.xlsx",
                        f_hsmt,
                        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                    ),
                ),
            ],
            data={
                "drawing_hint": "Bản vẽ cống và dự toán đi kèm",
                "auto_analyze": "false",
            },
            headers=AUTH_HEADERS,
        )

    assert resp.status_code == 201
    data = resp.json()
    assert data["status"] == "success"
    assert data["drawings_count"] == 1
    assert data["hsmts_count"] == 1
    assert len(data["takeoffs"]) == 1
    assert (
        data["takeoffs"][0]["attached_hsmt_file"] == "TIEN_LUONG_DU_TOAN_CONG_HOP.xlsx"
    )

    # Clean up
    db_client = ErpDatabaseClient()
    db_client.delete_drawing_takeoff(data["takeoffs"][0]["id"])


def test_dual_pdf_upload_auto_classification_and_pairing(tmp_path):
    """Kiểm thử trường hợp ĐẶC BIỆT: Cả 2 file tải lên ĐỀU LÀ PDF (1 PDF Bản vẽ A3 + 1 PDF Dự toán A4)."""
    import pymupdf

    service = DrawingTakeoffService()

    # 1. Tạo File 1: PDF Bản vẽ CAD A3 Landscape (1190 x 842 pt)
    drawing_pdf_path = tmp_path / "TAP_01_HO_SO_THIET_KE_BAN_VE.pdf"
    doc_dwg = pymupdf.open()
    p_dwg = doc_dwg.new_page(width=1190, height=842)
    p_dwg.draw_rect(pymupdf.Rect(50, 50, 1140, 792), color=(0, 0, 1), width=2)
    p_dwg.insert_text(
        (800, 750),
        "KHUNG TÊN BẢN VẼ: CỐNG HỘP PHONG CẦU\nTỶ LỆ: 1/50\nKÝ HIỆU: BVTC-01",
        fontsize=14,
    )
    doc_dwg.save(str(drawing_pdf_path))
    doc_dwg.close()

    # 2. Tạo File 2: PDF Hồ sơ mời thầu / Báo cáo KTKT A4 Portrait (595 x 842 pt)
    hsmt_pdf_path = tmp_path / "TAP_02_THUYET_MINH_TIEN_LUONG_DU_TOAN.pdf"
    doc_hsmt = pymupdf.open()
    p_hsmt = doc_hsmt.new_page(width=595, height=842)
    p_hsmt.insert_text(
        (50, 80),
        "CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM\nĐộc lập - Tự do - Hạnh phúc\n\nBÁO CÁO KINH TẾ KỸ THUẬT & TIÊN LƯỢNG MỜI THẦU\nCHỦ ĐẦU TƯ: UBND XÃ KIẾN MINH\nĐƠN GIÁ DỰ THẦU & BẢNG TỔNG HỢP KINH PHÍ\nCHƯƠNG V: YÊU CẦU VỀ KỸ THUẬT VÀ KHỐI LƯỢNG",
        fontsize=11,
    )
    doc_hsmt.save(str(hsmt_pdf_path))
    doc_hsmt.close()

    # 3. Test Service Deep PDF Classifier trực tiếp
    dwg_class = service.classify_pdf_content_type(drawing_pdf_path)
    assert dwg_class["is_drawing"] is True
    assert dwg_class["is_hsmt"] is False

    hsmt_class = service.classify_pdf_content_type(hsmt_pdf_path)
    assert hsmt_class["is_hsmt"] is True
    assert hsmt_class["is_drawing"] is False

    # 4. Test Batch Upload Endpoint khi nhận cùng lúc 2 file PDF
    with open(drawing_pdf_path, "rb") as f_dwg, open(hsmt_pdf_path, "rb") as f_hsmt:
        resp = client.post(
            "/v1/takeoff/upload-batch",
            files=[
                (
                    "files",
                    ("TAP_01_HO_SO_THIET_KE_BAN_VE.pdf", f_dwg, "application/pdf"),
                ),
                (
                    "files",
                    (
                        "TAP_02_THUYET_MINH_TIEN_LUONG_DU_TOAN.pdf",
                        f_hsmt,
                        "application/pdf",
                    ),
                ),
            ],
            data={
                "drawing_hint": "Bản vẽ và HSMT cùng định dạng PDF",
                "auto_analyze": "false",
            },
            headers=AUTH_HEADERS,
        )

    assert resp.status_code == 201
    data = resp.json()
    assert data["status"] == "success"
    assert data["drawings_count"] == 1, "Phải nhận diện đúng 1 file là Bản vẽ kỹ thuật"
    assert data["hsmts_count"] == 1, "Phải nhận diện đúng 1 file là HSMT/Dự toán"
    assert len(data["takeoffs"]) == 1
    assert (
        data["takeoffs"][0]["attached_hsmt_file"]
        == "TAP_02_THUYET_MINH_TIEN_LUONG_DU_TOAN.pdf"
    )

    # Clean up
    db_client = ErpDatabaseClient()
    db_client.delete_drawing_takeoff(data["takeoffs"][0]["id"])


def test_export_takeoff_excel_endpoint(tmp_path: Path):
    """Kiểm thử tính năng xuất toàn bộ bảng bóc tách BoQ và Đơn giá thị trường ra file Excel (.xlsx) chuẩn doanh nghiệp."""
    service = get_drawing_takeoff_service()
    pdf_path = create_sample_drawing_pdf(tmp_path, "sample_takeoff_for_excel.pdf")

    created = service.create_pending_takeoff(
        file_path=pdf_path,
        filename="sample_takeoff_for_excel.pdf",
        drawing_hint="Xuất Excel Test",
    )
    takeoff_id = str(created["id"])

    # 2. Thêm 3 items mẫu vào takeoff
    db_client = ErpDatabaseClient()
    db_client.create_drawing_takeoff_item(
        {
            "takeoff_id": takeoff_id,
            "item_order": 1,
            "wbs_code": "1.1",
            "norm_code": "AB.11110",
            "item_name": "Đào móng công trình đất cấp I",
            "category": "earthwork",
            "dimension_formula": "100m x 2m x 1.5m",
            "unit": "m3",
            "quantity": 300.0,
            "unit_price_vnd": 48000.0,
            "total_amount_vnd": 14400000.0,
        }
    )
    db_client.create_drawing_takeoff_item(
        {
            "takeoff_id": takeoff_id,
            "item_order": 2,
            "wbs_code": "2.1",
            "norm_code": "AF.11110",
            "item_name": "Đổ bê tông móng M250 đá 1x2",
            "category": "concrete",
            "dimension_formula": "100m x 2m x 0.4m",
            "unit": "m3",
            "quantity": 80.0,
            "unit_price_vnd": 2188000.0,
            "total_amount_vnd": 175040000.0,
        }
    )

    # 3. Gọi endpoint GET /v1/takeoff/{takeoff_id}/export-excel
    resp = client.get(f"/v1/takeoff/{takeoff_id}/export-excel", headers=AUTH_HEADERS)
    assert resp.status_code == 200
    assert (
        resp.headers["content-type"]
        == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    assert "Du_Toan_BoQ" in resp.headers["content-disposition"]
    assert len(resp.content) > 1000

    # 4. Kiểm tra cấu trúc nội dung file Excel trả về bằng openpyxl
    import openpyxl

    wb = openpyxl.load_workbook(io.BytesIO(resp.content))
    assert len(wb.sheetnames) == 4
    assert "1. TH Kinh Phí (NĐ 206)" in wb.sheetnames
    assert "2. BoQ Khối Lượng" in wb.sheetnames
    assert "3. Đơn Giá Chi Tiết (VL+NC+M)" in wb.sheetnames
    assert "4. Tổng Hợp Vật Tư (MR)" in wb.sheetnames

    ws1 = wb["2. BoQ Khối Lượng"]
    assert "CÔNG TY TNHH XÂY DỰNG ĐỊNH SƠN" in str(ws1["A1"].value)

    # 5. Clean up
    db_client.delete_drawing_takeoff(takeoff_id)


def test_chat_train_streaming_and_clear_history_endpoint(tmp_path: Path):
    """Kiểm tra phản hồi SSE streaming thời gian thực và làm mới lịch sử huấn luyện của AI Quỳnh."""
    from fastapi.testclient import TestClient

    from app.core.postgres.erp_client import ErpDatabaseClient
    from app.main import app
    from app.modules.takeoff.application.drawing_takeoff_service import get_drawing_takeoff_service

    client = TestClient(app)
    db = ErpDatabaseClient()
    service = get_drawing_takeoff_service()

    # 1. Create a pending takeoff
    pdf_path = create_sample_drawing_pdf(tmp_path, "sample_stream_test.pdf")
    created = service.create_pending_takeoff(
        file_path=pdf_path,
        filename="sample_stream_test.pdf",
        drawing_hint="Bản vẽ Test Phản Hồi Realtime AI Quỳnh",
    )
    takeoff_id = str(created["id"])

    try:
        # 2. Test SSE streaming endpoint
        stream_res = client.post(
            f"/v1/takeoff/{takeoff_id}/chat-train-stream",
            json={
                "feedback_message": "Đào vận chuyển đất cấp I 100m3 12.555 7,500,000 đ/100m3",
                "auto_apply_corrections": True,
            },
            headers=AUTH_HEADERS,
        )
        assert stream_res.status_code == 200
        assert "text/event-stream" in stream_res.headers["content-type"]

        body_text = stream_res.text
        assert "event: thinking" in body_text
        assert "event: delta" in body_text
        assert "event: complete" in body_text

        # 3. Test chat history list
        hist_res = client.get(
            f"/v1/takeoff/{takeoff_id}/chat-history", headers=AUTH_HEADERS
        )
        assert hist_res.status_code == 200
        msgs = hist_res.json().get("data", [])
        assert len(msgs) >= 2
        for m in msgs:
            assert "message" in m
            assert "message_text" in m
            assert len(m["message_text"]) > 0

        # 4. Test learned rules list has normalized aliases
        rules_res = client.get("/v1/takeoff/learned-rules/list", headers=AUTH_HEADERS)
        assert rules_res.status_code == 200
        rules = rules_res.json().get("data", [])
        assert len(rules) >= 1
        for r in rules:
            assert r.get("rule_title")
            assert r.get("rule_category")
            assert r.get("trigger_pattern")
            assert r.get("correction_directive")
            assert r.get("created_by_role")

        # 5. Test clear chat history endpoint
        del_res = client.delete(
            f"/v1/takeoff/{takeoff_id}/chat-history", headers=AUTH_HEADERS
        )
        assert del_res.status_code == 200
        assert del_res.json()["status"] == "success"

        hist_res2 = client.get(
            f"/v1/takeoff/{takeoff_id}/chat-history", headers=AUTH_HEADERS
        )
        assert len(hist_res2.json().get("data", [])) == 0

    finally:
        # Cleanup
        with db.get_connection() as conn:
            with conn.cursor() as cur:
                cur.execute(
                    "DELETE FROM erp_ai_takeoff_learned_rules WHERE takeoff_id = %s OR error_pattern LIKE %s;",
                    (takeoff_id, "%12.555%"),
                )
                conn.commit()
        db.delete_drawing_takeoff(takeoff_id)


def test_boq_audit_engine_price_quality_gates():
    """Test 4 Automated Price Quality Gates (AUDIT-PRC-01 to AUDIT-PRC-04) in BoqAuditEngine."""
    from app.modules.takeoff.application.boq_audit_engine import BoqAuditEngine

    # 1. Flawed BoQ items with 4 major pricing anomalies
    flawed_items = [
        {
            "item_name": "Đổ bê tông lót móng M100 đá 4x6",
            "unit": "m3",
            "quantity": 10.0,
            "unit_price": 1180000,
        },
        {
            "item_name": "Đổ bê tông kết cấu móng M250 đá 1x2",
            "unit": "m3",
            "quantity": 50.0,
            "unit_price": 1180000,
        },
        {
            "item_name": "Ván khuôn thép móng",
            "unit": "100m2",
            "quantity": 2.0,
            "unit_price": 1850000,
        },
        {
            "item_name": "Bốc xếp cát rời lên xe",
            "unit": "m3",
            "quantity": 100.0,
            "unit_price": 250000,
        },
        {
            "item_name": "Đóng cừ Larsen IV trên cạn",
            "unit": "100m",
            "quantity": 5.0,
            "unit_price": 4500000,
        },
        {
            "item_name": "Nhổ cừ Larsen IV dưới nước",
            "unit": "100m",
            "quantity": 5.0,
            "unit_price": 10000000,
        },
    ]

    flawed_audit = BoqAuditEngine.audit_takeoff(
        flawed_items, drawing_type="culvert", takeoff_title="Flawed Test"
    )
    codes = [f.get("code") for f in flawed_audit["findings"]]

    assert "AUDIT-PRC-01" in codes  # Concrete M250 <= M100 anomaly
    assert "AUDIT-PRC-02" in codes  # Formwork steel unit scale anomaly
    assert "AUDIT-PRC-03" in codes  # Sand handling ceiling anomaly
    assert "AUDIT-PRC-04" in codes  # Larsen extraction > driving anomaly
    assert flawed_audit["quality_score"] < 80

    # 2. Corrected / Standardized BoQ items
    healed_items = [
        {
            "item_name": "Đổ bê tông lót móng M100 đá 4x6",
            "unit": "m3",
            "quantity": 10.0,
            "unit_price": 950000,
        },
        {
            "item_name": "Đổ bê tông kết cấu móng M250 đá 1x2",
            "unit": "m3",
            "quantity": 50.0,
            "unit_price": 1480000,
        },
        {
            "item_name": "Ván khuôn thép móng",
            "unit": "100m2",
            "quantity": 2.0,
            "unit_price": 16500000,
        },
        {
            "item_name": "Bốc xếp cát rời lên xe",
            "unit": "m3",
            "quantity": 100.0,
            "unit_price": 22000,
        },
        {
            "item_name": "Đóng cừ Larsen IV trên cạn",
            "unit": "100m",
            "quantity": 5.0,
            "unit_price": 4500000,
        },
        {
            "item_name": "Nhổ cừ Larsen IV dưới nước",
            "unit": "100m",
            "quantity": 5.0,
            "unit_price": 3500000,
        },
    ]

    healed_audit = BoqAuditEngine.audit_takeoff(
        healed_items, drawing_type="culvert", takeoff_title="Healed Test"
    )
    healed_codes = [f.get("code") for f in healed_audit["findings"]]

    assert "AUDIT-PRC-01" not in healed_codes
    assert "AUDIT-PRC-02" not in healed_codes
    assert "AUDIT-PRC-03" not in healed_codes
    assert "AUDIT-PRC-04" not in healed_codes
    assert healed_audit["quality_score"] >= 85
