"""Structural-parity tests for lfcs-loo-builder + lfcs-boq-response-builder.

Tests the renderers DIRECTLY (bypassing Airtable) against synthetic 2629
(lump-sum-letter, $267,237) + HCB (BOQ-response, $434,462) fixtures.

Verifies:
  - LoO: 7 sections present, lump-sum words+figures match, validity clause,
    Standard Exclusions verbatim, mandatory clauses flagged, Layer 2 sanitisation pass
  - BOQ-response: header + body + subtotal + margin + contract-sum rows present;
    formula cells (=SUM, =Cn*En) actually formulae not values;
    hygiene patterns 6+7 present; Layer 2 sanitisation pass

Run: python test_harness/test_template_builders.py
"""

from __future__ import annotations

import os
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
ROOT = HERE.parent
sys.path.insert(0, str(ROOT))

from lib import loo_builder, sanitise, verify  # noqa: E402


def _print_pass(label: str, passed: bool, detail: str = "") -> None:
    status = "PASS" if passed else "FAIL"
    line = f"  [{status}] {label}"
    if detail:
        line += f" — {detail}"
    print(line)


# ---- LoO direct-render test (bypass Airtable) ----

def test_loo_render_2629_shape() -> dict:
    """Direct render: pass canonical 2629 inputs to the markdown renderer (skip Airtable read)."""
    print("\n=== TEST: LoO renderer (2629 shape, $267,237) ===")
    inputs = loo_builder.LoOInputs(
        bid_record_id="recDUMMY2629",  # not used in direct-render test
        recipient_company="The Civil Experts Pty Ltd",
        recipient_address="123 Civil Way\nSydney NSW",
        package_name="FRP Package — WWTP Slab",
        operator_of_record="Liam Fitzgerald",
        submission_date="07 May 2026",
        validity_days=30,
    )
    md = loo_builder._render_loo_markdown(
        inputs=inputs,
        job_no="2629",
        project_name="Erskine Park WWTP Slab",
        submission_date="07 May 2026",
        lump_sum_words=loo_builder._num_to_words(267_237.00),
        lump_sum_figures="$267,237.00 ex GST",
        standard_exclusions=loo_builder.STANDARD_EXCLUSIONS,
        prelim_assumptions=loo_builder.STANDARD_PRELIMINARIES_ASSUMPTIONS,
        detailed_exclusions=[
            "Site setup beyond LFCS scope",
            "Demolition not separately priced",
            "Asbestos handling",
            "Service location, identification, protection",
            "Latent conditions of any kind",
            "Concrete supply where head-contractor supplying",
            "Reo supply where head-contractor supplying",
            "Dewatering setup, monitoring, disposal",
            "Engineering certification beyond standard",
            "Programme constraints beyond RFQ",
        ],
        detailed_assumptions=[
            "Assumes both abutments can be poured at the same time.",
            "Assumes cranage will be provided by the client for installation and removal of bridge edge protection.",
            "Allows for 4 week hire of access scaffolding. We have provided rates for weekly hire of access scaffolding, if hire period of over 4 weeks is required.",
            "We have noted discrepancy in quantities from our take off and the pricing schedule. We have updated the submission schedule and highlighted our changes in quantities.",
            "We have added diaphragm beams at the abutments, which is missing from the pricing schedule.",
            "We have provided optional rate for sacrificial ply sheets.",
        ],
        detailed_inclusions=[
            "Supply, erect and strip formwork to all elements",
            "Place concrete to all elements",
            "Install reo (HC-supply) to all elements",
            "Class 2 finish per spec",
        ],
    )

    layer1 = sanitise.sanitise_markdown(md, operator_of_record="Liam Fitzgerald")
    layer2 = verify.verify_text(layer1.payload, location="loo-2629-test")

    sections = loo_builder._detect_sections(md)
    results = {
        "all_7_sections_present": len(sections) == 7,
        "sections_found": sections,
        "lump_sum_words_present": "Two Hundred Sixty-Seven Thousand Two Hundred Thirty-Seven Dollars" in md,
        "lump_sum_figures_present": "$267,237.00 ex GST" in md,
        "validity_30_days_present": "30 days" in md,
        "signature_liam_present": "Liam Fitzgerald" in md,
        "standard_exclusions_count_10": md.count("\n- ") >= 10,
        "discrepancy_clause_present": "discrepancy" in md.lower(),
        "scope_gap_clause_present": "missing from" in md.lower() or "scope-gap" in md.lower(),
        "cranage_clause_present": "cranage" in md.lower(),
        "hire_duration_clause_present": "week hire" in md.lower(),
        "optional_clause_present": "optional" in md.lower(),
        "layer1_strips_zero_on_clean_input": len(layer1.regex_strips_applied) == 0,
        "layer2_passed": layer2.passed,
        "layer2_findings_count": len(layer2.findings),
    }

    pass_count = 0
    total = 0
    for k, v in results.items():
        if isinstance(v, bool):
            total += 1
            if v:
                pass_count += 1
            _print_pass(k, v)
        elif isinstance(v, list):
            _print_pass(k, len(v) > 0, f"list of {len(v)}")
        else:
            _print_pass(k, True, f"{v}")

    return {"results": results, "pass_count": pass_count, "total_bools": total, "md_sample": md[:600]}


def test_loo_synthetic_leak_blocked():
    """Inject agent-attribution token mid-document; verify Layer 2 fails closed."""
    print("\n=== TEST: LoO sanitisation fail-closed on synthetic leak ===")
    bad_md = """
# LETTER OF OFFER

Some legitimate body text.

Hidden leak: claude-code-opsman first-pass rate. [source: friend-vault] tag here.

## STANDARD EXCLUSIONS

- All surface water and groundwater management.
"""
    layer1 = sanitise.sanitise_markdown(bad_md, operator_of_record="Liam Fitzgerald")
    layer2 = verify.verify_text(layer1.payload, location="loo-synthetic-leak")
    layer2_raw = verify.verify_text(bad_md, location="loo-bypass-l1")

    results = {
        "L1_stripped_attribution_patterns": len(layer1.regex_strips_applied) >= 2,
        "L2_passes_after_L1_clean": layer2.passed,
        "L2_fails_on_raw_bad_input": not layer2_raw.passed,
        "L2_finds_leaks_in_raw": len(layer2_raw.findings) >= 2,
    }
    pass_count = sum(1 for v in results.values() if v)
    for k, v in results.items():
        _print_pass(k, v)
    return {"results": results, "pass_count": pass_count, "total": len(results)}


def test_boq_response_xlsx_render():
    """Build a BOQ-response xlsx from synthetic line items (skipping Airtable)."""
    print("\n=== TEST: BOQ-response renderer (HCB shape, $434,462) ===")
    try:
        from openpyxl import Workbook, load_workbook
        from openpyxl.styles import Font, PatternFill
    except ImportError:
        print("  [SKIP] openpyxl unavailable")
        return {"skipped": True}

    output_dir = HERE / "output_boq_test"
    output_dir.mkdir(parents=True, exist_ok=True)
    raw_path = output_dir / "test-BOQ-Response-RAW.xlsx"
    sanitised_path = output_dir / "test-BOQ-Response-LFCS-submitted.xlsx"

    # Synthetic line items mirroring HCB shape (8 representative lines + ADDED + EXCLUDED markers)
    lines = [
        ("6.2.1", "Supply + erect formwork to abutments", 207.92, "m2", 207.87, ""),
        ("6.3.1", "Strip formwork to abutments — included in Item 6.2.1", 0, "m2", 0, "Included in Item 6.2.1"),
        ("7.1.1", "Supply + erect formwork to bridge deck", 256.5, "m2", 624.20, ""),
        ("7.2.3.2", "Install expansion joints — Included in Item 7.2.3.1", 0, "m", 0, "Included in Item 7.2.3.1"),
        ("ADDED", "Diaphragm beam formwork — missing from pricing schedule", 21.0, "m2", 700.40, "ADDED — missing from HC schedule"),
        ("ADDED", "Bridge edge protection hire", 6, "week", 1500, "Rate Only — Allows for 6 week hire"),
        ("OPTION", "Sacrificial ply sheets for super-T girders", 24, "m2", 57, "OPTION — client elect"),
        ("6.1.1", "Cut + remove section of southern wall — EXCLUDED", 0, "Item", 0, "EXCLUDED — by-others"),
    ]

    wb = Workbook()
    ws = wb.active
    ws.title = "BOQ-Response"

    # Headers
    headers = ["Bill Ref", "Description", "Qty", "Unit", "Rate ex GST", "Total ex GST", "Notes / Calc Basis"]
    for i, h in enumerate(headers, start=1):
        cell = ws.cell(row=1, column=i, value=h)
        cell.font = Font(bold=True)
        cell.fill = PatternFill("solid", fgColor="DDDDDD")

    # Body
    hygiene_present = {"6_consistent_units"}
    for r, (bill_ref, desc, qty, unit, rate, notes) in enumerate(lines, start=2):
        ws.cell(row=r, column=1, value=bill_ref)
        ws.cell(row=r, column=2, value=desc)
        ws.cell(row=r, column=3, value=qty)
        ws.cell(row=r, column=4, value=unit)
        ws.cell(row=r, column=5, value=rate)
        if qty and rate:
            ws.cell(row=r, column=6, value=f"=C{r}*E{r}")
        ws.cell(row=r, column=7, value=notes)
        if "ADDED" in str(bill_ref) or "ADDED" in notes:
            hygiene_present.add("1_ADDED_rows")
        if "Included in Item" in notes:
            hygiene_present.add("2_included_in_xref")
        if "Rate Only" in notes:
            hygiene_present.add("3_Rate_Only")
        if "OPTION" in str(bill_ref) or "OPTION" in notes:
            hygiene_present.add("4_OPTION_rows")
        if "EXCLUDED" in notes:
            hygiene_present.add("5_EXCLUDED_visible")

    last_body_row = len(lines) + 1
    subtotal_row = last_body_row + 2
    ws.cell(row=subtotal_row, column=2, value="Construction Sub-total ex GST").font = Font(bold=True)
    ws.cell(row=subtotal_row, column=6, value=f"=SUM(F2:F{last_body_row})")
    ws.cell(row=subtotal_row, column=6).font = Font(bold=True)
    margin_row = subtotal_row + 1
    ws.cell(row=margin_row, column=2, value="Loaded uplift (26.5%)")
    ws.cell(row=margin_row, column=6, value=f"=F{subtotal_row}*0.265")
    contract_row = margin_row + 2
    ws.cell(row=contract_row, column=2, value="Contract Sum ex GST").font = Font(bold=True)
    ws.cell(row=contract_row, column=6, value=f"=F{subtotal_row}+F{margin_row}")
    ws.cell(row=contract_row, column=6).font = Font(bold=True)
    hygiene_present.add("7_formula_section_totals")

    wb.save(raw_path)

    # Apply Layer 1 + Layer 2
    l1 = sanitise.sanitise_xlsx(str(raw_path), str(sanitised_path), operator_of_record="Liam Fitzgerald")
    l2 = verify.verify_xlsx(str(sanitised_path), expected_author="Liam Fitzgerald")

    # Re-load sanitised xlsx to verify formulas survived (formulas, not flattened values)
    wb_check = load_workbook(str(sanitised_path), data_only=False)
    ws_check = wb_check.active
    formula_cells_intact = (
        str(ws_check.cell(row=2, column=6).value or "").startswith("=")
        and str(ws_check.cell(row=subtotal_row, column=6).value or "").startswith("=SUM(")
        and str(ws_check.cell(row=margin_row, column=6).value or "").startswith("=")
    )

    results = {
        "L1_runs_xlsx_clean": l1.layer1_pass,
        "L1_metadata_overrides_3_props": len(l1.metadata_overrides) >= 3,
        "L2_passed": l2.passed,
        "live_formulas_survived_sanitise": formula_cells_intact,
        "header_row_present": ws_check.cell(1, 1).value == "Bill Ref",
        "subtotal_row_present": "Construction Sub-total" in str(ws_check.cell(subtotal_row, 2).value),
        "margin_row_present": "Margin" in str(ws_check.cell(margin_row, 2).value),
        "contract_sum_row_present": "Contract Sum" in str(ws_check.cell(contract_row, 2).value),
        "hygiene_patterns_count": len(hygiene_present),
        "hygiene_patterns_present": sorted(hygiene_present),
    }
    pass_count = 0
    total = 0
    for k, v in results.items():
        if isinstance(v, bool):
            total += 1
            if v:
                pass_count += 1
            _print_pass(k, v)
        else:
            _print_pass(k, True, f"{v}")
    return {"results": results, "pass_count": pass_count, "total_bools": total, "xlsx_path": str(sanitised_path)}


def main() -> int:
    print("=" * 70)
    print("Template Builder Tests — lfcs-loo-builder + lfcs-boq-response-builder")
    print("=" * 70)

    r1 = test_loo_render_2629_shape()
    r2 = test_loo_synthetic_leak_blocked()
    r3 = test_boq_response_xlsx_render()

    print("\n" + "=" * 70)
    print("SUMMARY")
    print("=" * 70)
    print(f"LoO 2629-shape:           {r1['pass_count']}/{r1['total_bools']} bool checks passed")
    print(f"LoO synthetic-leak block: {r2['pass_count']}/{r2['total']} bool checks passed")
    if r3.get("skipped"):
        print("BOQ-response: SKIPPED (openpyxl unavailable)")
    else:
        print(f"BOQ-response xlsx:        {r3['pass_count']}/{r3['total_bools']} bool checks passed")

    overall_pass = (
        r1["pass_count"] == r1["total_bools"]
        and r2["pass_count"] == r2["total"]
        and (r3.get("skipped") or r3["pass_count"] == r3["total_bools"])
    )
    print(f"\nOverall: {'PASS' if overall_pass else 'FAIL'}")
    return 0 if overall_pass else 1


if __name__ == "__main__":
    sys.exit(main())
