#!/usr/bin/env python3
"""Regression test for Phase 0 — the 4-stage pipeline funnel + the derived grid signals
(ROADMAP "Pipeline stages + investor flags/labels -- LOCKED SPEC", 2026-06-19).

Covers the parts the round-trip test in test_grid_pipeline_link.py only checks structurally:

  * migration 0007 stage remap — outreach/meeting -> engaged, due_diligence -> diligence,
    committed/funded -> commitment, and the stray 'lost' value archived (soft-deleted), with
    'lead' left untouched;
  * existing_investor_by_source_row — total_invested > 0 is the auto-derived "Existing Investor";
  * staleness_by_source_row — last-contact age maps to '' / 'aging' (>= 30d) / 'stale' (>= 60d),
    boundaries inclusive, and NO recorded activity -> '' (no false "stale" on a fresh lead).

Synthetic only (guardrail #9). Run: cd backend && python3 test_pipeline_stages_v2.py
"""
import os
import sqlite3
import sys
import tempfile
from datetime import datetime, timedelta

_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 = []
_HERE = os.path.dirname(os.path.abspath(__file__))


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


def _days_ago(n):
    return (datetime.utcnow() - timedelta(days=n)).strftime("%Y-%m-%d")


def test_migration_remap(conn):
    print("\n[migration 0007: stage remap + lost archived]")
    c = conn.cursor()
    olds = [("m_out", "outreach"), ("m_meet", "meeting"), ("m_dd", "due_diligence"),
            ("m_comm", "committed"), ("m_fund", "funded"), ("m_lost", "lost"),
            ("m_lead", "lead")]
    for oid, stage in olds:
        c.execute("INSERT INTO opportunities (id, name, stage, contact_id, owner_id) VALUES (?,?,?,?,?)",
                  (oid, oid, stage, "c0", "u1"))
    conn.commit()
    # init_db already applied 0007; re-run its SQL against the rows we just inserted with
    # legacy values (the UPDATEs target by old stage value, so this is exactly the remap).
    with open(os.path.join(_HERE, "migrations", "0007_pipeline_stages_v2.sql")) as fh:
        conn.executescript(fh.read())
    conn.commit()

    def stage_of(oid):
        return c.execute("SELECT stage FROM opportunities WHERE id=?", (oid,)).fetchone()[0]

    def archived(oid):
        return c.execute("SELECT deleted_at FROM opportunities WHERE id=?", (oid,)).fetchone()[0] is not None

    check(stage_of("m_out") == "engaged" and stage_of("m_meet") == "engaged",
          "outreach + meeting -> engaged")
    check(stage_of("m_dd") == "diligence", "due_diligence -> diligence")
    check(stage_of("m_comm") == "commitment" and stage_of("m_fund") == "commitment",
          "committed + funded -> commitment")
    check(stage_of("m_lead") == "lead", "lead unchanged")
    check(archived("m_lost"), "lost opp archived (deleted_at set)")
    check(not archived("m_lead") and not archived("m_comm"),
          "non-lost opps NOT archived by the migration")
    # Cleanup so these rows don't perturb the derivation seed below.
    c.execute("DELETE FROM opportunities WHERE id IN ('m_out','m_meet','m_dd','m_comm','m_fund','m_lost','m_lead')")
    conn.commit()


def _investor(conn, srid, total, contact_id=None, comm_days_ago=None):
    """Seed a grid investor (+ optional linked contact & dated communication)."""
    iid = "i_" + srid
    conn.execute("INSERT INTO fundraising_investors (id, investor_name, source_row_id, total_invested) "
                 "VALUES (?,?,?,?)", (iid, srid, srid, total))
    if contact_id:
        conn.execute("INSERT INTO contacts (id, first_name, last_name) VALUES (?,?,?)",
                     (contact_id, srid, "Person"))
        conn.execute("INSERT INTO fundraising_contacts (id, investor_id, full_name, contact_id) "
                     "VALUES (?,?,?,?)", ("fc_" + srid, iid, srid + " Person", contact_id))
        if comm_days_ago is not None:
            conn.execute("INSERT INTO communications (id, contact_id, communication_date, created_by, subject) "
                         "VALUES (?,?,?,?,?)",
                         ("cm_" + srid, contact_id, _days_ago(comm_days_ago), "u1", "note"))


def test_derivations(conn):
    print("\n[existing_investor + staleness derivations]")
    # Existing flag: only total_invested > 0.
    _investor(conn, "rowExist", 5_000_000, contact_id="c_exist", comm_days_ago=100)
    _investor(conn, "rowProspect", 0)
    # Staleness ramp + boundaries (>=30 aging, >=60 stale; inclusive).
    _investor(conn, "rowStale", 0, contact_id="c_stale", comm_days_ago=70)
    _investor(conn, "rowAging", 0, contact_id="c_aging", comm_days_ago=45)
    _investor(conn, "rowFresh", 0, contact_id="c_fresh", comm_days_ago=5)
    _investor(conn, "rowNoAct", 0)
    _investor(conn, "rowB60", 0, contact_id="c_b60", comm_days_ago=60)   # boundary -> stale
    _investor(conn, "rowB59", 0, contact_id="c_b59", comm_days_ago=59)   # -> aging
    _investor(conn, "rowB30", 0, contact_id="c_b30", comm_days_ago=30)   # boundary -> aging
    _investor(conn, "rowB29", 0, contact_id="c_b29", comm_days_ago=29)   # -> fresh
    # Empty source_row_id with committed capital — must be EXCLUDED by the `if not srid` guard
    # (would otherwise key the injection under '' and clobber a real row).
    _investor(conn, "", 9_999, contact_id="c_empty", comm_days_ago=100)
    conn.commit()

    existing = server.existing_investor_by_source_row(conn)
    check(existing == {"rowExist"},
          f"existing_investor = total_invested>0 with a non-empty source_row_id only (got {sorted(existing)})")

    st = server.staleness_by_source_row(conn)
    level = lambda srid: st.get(srid, (None, "MISSING"))[1]
    check(level("rowStale") == "stale", f"70d -> stale (got {level('rowStale')})")
    check(level("rowAging") == "aging", f"45d -> aging (got {level('rowAging')})")
    check(level("rowFresh") == "", f"5d -> fresh/'' (got {level('rowFresh')!r})")
    check(level("rowNoAct") == "", f"no activity -> '' (got {level('rowNoAct')!r})")
    check(level("rowExist") == "stale", "existing + stale coexist (orthogonal axes)")
    check(level("rowB60") == "stale" and level("rowB59") == "aging",
          f"60d boundary inclusive -> stale; 59d -> aging (got {level('rowB60')}, {level('rowB59')})")
    check(level("rowB30") == "aging" and level("rowB29") == "",
          f"30d boundary inclusive -> aging; 29d -> '' (got {level('rowB30')}, {level('rowB29')!r})")
    # last_activity_at is carried through alongside the level for the recency display.
    check(st.get("rowStale", (None, ""))[0] is not None, "stale row carries a last_activity_at value")
    check(st.get("rowNoAct", ("X", ""))[0] is None, "no-activity row has last_activity_at None")


def main():
    server.init_db()
    conn = server.get_db()
    conn.execute("INSERT INTO users (id,username,email,password_hash,full_name,role,is_active) "
                 "VALUES ('u1','grant','grant@ten31.example','x','Grant','admin',1)")
    conn.execute("INSERT INTO contacts (id, first_name, last_name) VALUES ('c0','Seed','Contact')")
    conn.commit()
    test_migration_remap(conn)
    test_derivations(conn)
    conn.close()
    print("\n" + ("ALL PASS (pipeline stages v2)" if not FAILS else f"{len(FAILS)} FAILED"))
    sys.exit(1 if FAILS else 0)


if __name__ == "__main__":
    main()
