#!/usr/bin/env python3
"""Tests for the mobile Contacts card's grid-derived signals (Phase 8a).

GET /api/contacts enriches each classic contact with two read-only, live-derived fields
sourced from the fundraising grid (the canonical investor model), for the mobile card:
  - `committed`  -> the linked investor's total_invested (>0 drives the existing-LP avatar ring),
                    mirroring existing_investor_by_source_row (committed capital, not graveyard);
  - `pipeline_stage` -> that investor's live derived stage (drives the card's stage pill),
                    or null when the investor isn't in the pipeline.
  - `priority` -> that investor's priority flag (drives the mobile Contacts Priority sort, 8d).
  - `source_row_id` -> that investor's grid row id (the "Open investor in Grid" deep-link target, 8h),
                    present for ANY grid-linked contact (even a zero-commit prospect), null otherwise.
A contact with no grid link (pure classic/legacy contact) gets committed 0 / stage null / priority false
/ source_row_id null.
Signals are derived fresh on read and never stored on the contact. Synthetic data only.

Run: cd backend && python3 test_contacts_grid_signals.py
"""
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
from http.server import ThreadingHTTPServer

_DATA = tempfile.mkdtemp()
os.environ["CRM_DATA_DIR"] = _DATA
os.environ["CRM_DB_PATH"] = os.path.join(_DATA, "crm.db")

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import server  # noqa: E402

FAILS = []


def check(cond, msg):
    print(("  PASS " if cond else "  FAIL ") + msg)
    if not cond:
        FAILS.append(msg)


class _Quiet(server.CRMHandler):
    def log_message(self, *a):
        pass


def _req(port, method, path, token=None, body=None):
    conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
    headers = {}
    if token:
        headers["Authorization"] = "Bearer " + token
    payload = None
    if body is not None:
        payload = json.dumps(body)
        headers["Content-Type"] = "application/json"
    conn.request(method, path, body=payload, headers=headers)
    resp = conn.getresponse()
    raw = resp.read().decode("utf-8", "replace")
    conn.close()
    data = None
    if raw:
        try:
            data = json.loads(raw)
        except ValueError:
            pass
    return resp.status, data


# One fund column so a non-zero cell rolls up into total_invested (the "existing LP" signal).
COLUMNS = [{"id": "fund1", "label": "Fund III", "isFund": True}]
ROW_ACME = {"id": "rowAcme", "investor_name": "Acme Capital", "priority": True, "fund1": 250000,
            "contacts": [{"name": "Jane Doe", "email": "jane@acme.com", "title": "GP"}]}
ROW_BETA = {"id": "rowBeta", "investor_name": "Beta Capital", "fund1": 0,
            "contacts": [{"name": "Pat Roe", "email": "pat@beta.com", "title": ""}]}


def _db():
    return sqlite3.connect(os.environ["CRM_DB_PATH"])


def seed():
    c = _db()
    c.execute("INSERT INTO users (id,username,email,password_hash,full_name,role,is_active) "
              "VALUES ('u1','grant','grant@ten31.example','x','Grant','admin',1)")
    # A pure classic contact with NO fundraising-grid link (not an investor).
    c.execute("INSERT INTO contacts (id,first_name,last_name,email,contact_type,status) "
              "VALUES ('cLegacy','Vendor','Vince','vince@vendor.com','other','active')")
    c.commit()
    c.close()


def _by_email(contacts, email):
    return next((c for c in contacts if (c.get("email") or "").lower() == email), None)


def main():
    server.init_db()
    seed()
    token = server.create_token("u1", "grant", "admin")

    httpd = ThreadingHTTPServer(("127.0.0.1", 0), _Quiet)
    port = httpd.server_address[1]
    threading.Thread(target=httpd.serve_forever, daemon=True).start()
    try:
        st, _ = _req(port, "PUT", "/api/fundraising/state", token,
                     {"grid": {"columns": COLUMNS, "rows": [ROW_ACME, ROW_BETA]}, "views": []})
        check(st == 200, f"seed grid via PUT /state (got {st})")

        # Put Acme into the pipeline at 'engaged' so its contact's card shows a stage pill.
        st, d = _req(port, "POST", "/api/fundraising/pipeline/link", token,
                     {"source_row_id": "rowAcme", "stage": "engaged"})
        check(st in (200, 201), f"link Acme to pipeline @engaged (got {st}, {d})")

        st, d = _req(port, "GET", "/api/contacts?limit=500", token)
        contacts = (d or {}).get("data") or []
        check(st == 200 and contacts, f"GET /api/contacts (got {st}, {len(contacts)} contacts)")

        jane = _by_email(contacts, "jane@acme.com")
        pat = _by_email(contacts, "pat@beta.com")
        vince = _by_email(contacts, "vince@vendor.com")
        check(jane is not None, "Acme's synced contact Jane Doe is in the directory")
        check(pat is not None, "Beta's synced contact Pat Roe is in the directory")
        check(vince is not None, "the pure classic contact Vince is in the directory")

        # ── existing-LP ring signal: committed reflects the linked investor's rollup ──
        print("\n[committed: existing-LP ring driven by the linked investor's total_invested]")
        check((jane or {}).get("committed") == 250000,
              f"Jane.committed == 250000 (existing LP) (got {(jane or {}).get('committed')})")
        check((pat or {}).get("committed") == 0,
              f"Pat.committed == 0 (zero-commit prospect, no ring) (got {(pat or {}).get('committed')})")
        check((vince or {}).get("committed") == 0,
              f"Vince.committed == 0 (no grid link) (got {(vince or {}).get('committed')})")

        # ── stage-pill signal: pipeline_stage is the investor's live derived stage ──
        print("\n[pipeline_stage: stage pill driven by the investor's live opp stage]")
        check((jane or {}).get("pipeline_stage") == "engaged",
              f"Jane.pipeline_stage == 'engaged' (got {(jane or {}).get('pipeline_stage')!r})")
        check((pat or {}).get("pipeline_stage") is None,
              f"Pat.pipeline_stage is None (not in pipeline) (got {(pat or {}).get('pipeline_stage')!r})")
        check((vince or {}).get("pipeline_stage") is None,
              f"Vince.pipeline_stage is None (no grid link) (got {(vince or {}).get('pipeline_stage')!r})")

        # ── priority signal: flagged investor → contact's Priority-sort key (8d) ──
        print("\n[priority: Contacts Priority sort driven by the investor's priority flag]")
        check((jane or {}).get("priority") is True,
              f"Jane.priority is True (Acme flagged) (got {(jane or {}).get('priority')!r})")
        check((pat or {}).get("priority") is False,
              f"Pat.priority is False (Beta not flagged) (got {(pat or {}).get('priority')!r})")
        check((vince or {}).get("priority") is False,
              f"Vince.priority is False (no grid link) (got {(vince or {}).get('priority')!r})")

        # ── source_row_id signal: the "Open investor in Grid" deep-link target (8h) ──
        print("\n[source_row_id: Open-in-Grid deep-link target = the linked investor's grid row id]")
        check((jane or {}).get("source_row_id") == "rowAcme",
              f"Jane.source_row_id == 'rowAcme' (got {(jane or {}).get('source_row_id')!r})")
        check((pat or {}).get("source_row_id") == "rowBeta",
              f"Pat.source_row_id == 'rowBeta' (present for a zero-commit linked contact) (got {(pat or {}).get('source_row_id')!r})")
        check((vince or {}).get("source_row_id") is None,
              f"Vince.source_row_id is None (no grid link) (got {(vince or {}).get('source_row_id')!r})")

        # ── the get-by-id endpoint carries the same signals (mobile detail sheet, 8b) ──
        print("\n[get-by-id: /api/contacts/{id} also injects committed + pipeline_stage]")
        st, d = _req(port, "GET", f"/api/contacts/{jane['id']}", token)
        detail = (d or {}).get("data") or {}
        check(st == 200 and detail.get("committed") == 250000 and detail.get("pipeline_stage") == "engaged"
              and detail.get("priority") is True,
              f"detail carries committed/pipeline_stage/priority (got committed={detail.get('committed')}, stage={detail.get('pipeline_stage')!r}, priority={detail.get('priority')!r})")
        st, d = _req(port, "GET", f"/api/contacts/{vince['id']}", token)
        vdetail = (d or {}).get("data") or {}
        check(st == 200 and vdetail.get("committed") == 0 and vdetail.get("pipeline_stage") is None
              and vdetail.get("priority") is False,
              f"unlinked contact detail has committed 0 / stage None / priority False (got {vdetail.get('committed')}, {vdetail.get('pipeline_stage')!r}, {vdetail.get('priority')!r})")

        # ── stage tracks the board: advancing the opp re-derives the contact's stage ──
        print("\n[derived-live: advancing the board stage re-derives the contact's pill]")
        opp_id = None
        st, d = _req(port, "GET", "/api/fundraising/state", token)
        for r in (d or {}).get("data", {}).get("grid", {}).get("rows", []):
            if r.get("id") == "rowAcme":
                opp_id = r.get("opportunity_id")
        st, _ = _req(port, "PATCH", f"/api/opportunities/{opp_id}/stage", token, {"stage": "diligence"})
        check(st == 200, f"advance Acme's opp -> diligence (got {st})")
        st, d = _req(port, "GET", "/api/contacts?limit=500", token)
        jane2 = _by_email((d or {}).get("data") or [], "jane@acme.com")
        check((jane2 or {}).get("pipeline_stage") == "diligence",
              f"Jane.pipeline_stage re-derives to 'diligence' (got {(jane2 or {}).get('pipeline_stage')!r})")

        # ── dedup: a contact linked to two investors exposes the highest-committed one ──
        print("\n[dedup: highest-committed linked investor wins for a multi-linked contact]")
        c = _db()
        # Link Jane's classic contact to a SECOND, richer investor (direct rows — the grid sync
        # makes one link per pill; this exercises the multi-link branch in contact_grid_signals).
        c.execute("INSERT INTO fundraising_investors (id, investor_name, source_row_id, total_invested) "
                  "VALUES ('inv2','Mega Fund LP','rowMega',500000)")
        c.execute("INSERT INTO fundraising_contacts (id, investor_id, full_name, contact_id) "
                  "VALUES ('fc2','inv2','Jane Doe',?)", (jane['id'],))
        c.commit()
        c.close()
        st, d = _req(port, "GET", f"/api/contacts/{jane['id']}", token)
        jd = (d or {}).get("data") or {}
        check(jd.get("committed") == 500000,
              f"multi-linked contact exposes the higher committed (500000 > 250000) (got {jd.get('committed')})")
        # The winning (higher-committed) link is Mega Fund LP, which is not flagged → priority follows it.
        check(jd.get("priority") is False,
              f"multi-linked contact's priority follows the higher-committed investor (Mega, unflagged) (got {jd.get('priority')!r})")
        # The deep-link target also follows the winning link → Mega's grid row (rowMega), not rowAcme.
        check(jd.get("source_row_id") == "rowMega",
              f"multi-linked contact's source_row_id follows the higher-committed investor (rowMega) (got {jd.get('source_row_id')!r})")
    finally:
        httpd.shutdown()

    print()
    if FAILS:
        print(f"FAILED ({len(FAILS)}):")
        for m in FAILS:
            print("  - " + m)
        sys.exit(1)
    print("All contacts-grid-signals tests passed.")


if __name__ == "__main__":
    main()
