"""
test_milestone4.py — Automated success tests for Milestone 4

Milestone 4 success checklist (from new-milestone.md):
  ✅ run.py is a runnable entry point with run_bots()
  ✅ voice/elevenlabs_tts generates MP3 with cross-platform path
  ✅ build_voice_text returns text for all 8 feature types
  ✅ UPI link generated for expense-type negotiations
  ✅ on_resolution in bot.py wires voice + UPI + analytics
  ✅ analytics stored in DB after negotiation
  ✅ run.py imports cleanly (all modules resolvable)
  ✅ bot.py imports cleanly (voice + UPI imports added)
  ✅ fairness score computed correctly
  ✅ No crashes when ElevenLabs key is missing (fallback to None)

Run from project root:
    cd e:\\negot8
    python test/test_milestone4.py
"""
import sys, os, asyncio, json, importlib

ROOT    = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BACKEND = os.path.join(ROOT, "backend")
BOTS    = os.path.join(BACKEND, "telegram-bots")
sys.path.insert(0, BACKEND)
sys.path.insert(0, BOTS)

PASS = "✅"
FAIL = "❌"
results = []

def record(ok: bool, name: str, detail: str = ""):
    results.append((PASS if ok else FAIL, name, detail))
    print(f"  {PASS if ok else FAIL}  {name}" + (f"  [{detail}]" if detail else ""))


# ═══════════════════════════════════════════════════════════════
# 1. Module import checks
# ═══════════════════════════════════════════════════════════════

def test_imports():
    print("\n── 1. Module imports ──")
    for mod, label in [
        ("run",                   "run.py imports cleanly"),
        ("bot",                   "bot.py imports cleanly"),
        ("voice.elevenlabs_tts",  "elevenlabs_tts imports cleanly"),
    ]:
        try:
            importlib.import_module(mod)
            record(True, label)
        except Exception as e:
            record(False, label, str(e)[:120])

    # Verify new symbols exist in bot.py
    try:
        import bot
        has_voice = hasattr(bot, "generate_voice_summary") or \
                    "generate_voice_summary" in getattr(bot, "__dict__", {}) or \
                    True   # imported into local scope via 'from ... import'
        from bot import _upi_tool, VOICE_ID_AGENT_A, VOICE_ID_AGENT_B
        record(True, "bot.py has _upi_tool, VOICE_ID constants")
    except ImportError as e:
        record(False, "bot.py has _upi_tool, VOICE_ID constants", str(e))

    # Verify run.py has run_bots
    try:
        from run import run_bots
        record(True, "run.py exposes run_bots()")
    except ImportError as e:
        record(False, "run.py exposes run_bots()", str(e))


# ═══════════════════════════════════════════════════════════════
# 2. run.py entry point structure
# ═══════════════════════════════════════════════════════════════

def test_run_py_structure():
    print("\n── 2. run.py structure ──")
    import inspect, run

    # run_bots is a coroutine function
    record(asyncio.iscoroutinefunction(run.run_bots),
           "run_bots() is an async function")

    # Source contains if __name__ == "__main__"
    try:
        src = open(os.path.join(BACKEND, "run.py")).read()
        record('if __name__ == "__main__"' in src,
               'run.py has if __name__ == "__main__" block')
        record("asyncio.run(run_bots())" in src,
               "run.py calls asyncio.run(run_bots())")
    except Exception as e:
        record(False, "run.py source check", str(e))


# ═══════════════════════════════════════════════════════════════
# 3. ElevenLabs TTS — cross-platform path + templates
# ═══════════════════════════════════════════════════════════════

def test_voice_module():
    print("\n── 3. Voice module ──")
    from voice.elevenlabs_tts import build_voice_text, VOICE_TEMPLATES

    # All 8 feature types have templates
    expected = {"expenses", "collaborative", "scheduling", "marketplace",
                "trip", "freelance", "roommate", "conflict"}
    actual = set(VOICE_TEMPLATES.keys())
    record(expected.issubset(actual),
           "All 8 feature types have voice templates",
           f"missing: {expected - actual}" if not expected.issubset(actual) else "all present")

    # build_voice_text returns safe string when keys missing
    text = build_voice_text("scheduling", {"rounds": 3, "date": "Monday", "time": "10am", "location": "Blue Tokai"})
    record(isinstance(text, str) and len(text) > 5,
           "build_voice_text returns non-empty string for scheduling",
           text[:60])

    text2 = build_voice_text("unknown_feature", {"rounds": 2, "summary": "done"})
    record(isinstance(text2, str) and "negotiation" in text2.lower(),
           "build_voice_text has safe fallback for unknown feature types",
           text2[:60])

    # Check no hardcoded /tmp/ path in tts module
    src = open(os.path.join(BACKEND, "voice", "elevenlabs_tts.py")).read()
    record("/tmp/" not in src,
           "elevenlabs_tts.py uses cross-platform path (no hardcoded /tmp/)")
    record("tempfile.gettempdir()" in src,
           "elevenlabs_tts.py uses tempfile.gettempdir()")


# ═══════════════════════════════════════════════════════════════
# 4. ElevenLabs TTS — graceful failure without API key
# ═══════════════════════════════════════════════════════════════

async def test_voice_graceful_failure():
    print("\n── 4. Voice graceful failure (no real API call) ──")
    import unittest.mock as mock
    from voice.elevenlabs_tts import generate_voice_summary

    # Patch httpx to simulate API error (401)
    class FakeResp:
        status_code = 401
        text = "Unauthorized"

    class FakeClient:
        async def __aenter__(self): return self
        async def __aexit__(self, *a): pass
        async def post(self, *a, **kw): return FakeResp()

    with mock.patch("httpx.AsyncClient", return_value=FakeClient()):
        result = await generate_voice_summary("test text", "neg_test_001")
    record(result is None,
           "generate_voice_summary returns None on API failure (no crash)")


# ═══════════════════════════════════════════════════════════════
# 5. UPI tool works
# ═══════════════════════════════════════════════════════════════

async def test_upi_tool():
    print("\n── 5. UPI tool ──")
    from tools.upi_generator import UPIGeneratorTool
    tool = UPIGeneratorTool()
    result = await tool.execute(
        payee_upi="alice@upi",
        payee_name="Alice",
        amount=500.0,
        note="negoT8: expenses settlement"
    )
    record("upi_link" in result and result["upi_link"].startswith("upi://"),
           "UPI link generated with upi:// scheme",
           result.get("upi_link", "")[:60])
    record("alice@upi" in result["upi_link"],
           "UPI link contains payee UPI ID")


# ═══════════════════════════════════════════════════════════════
# 6. Analytics DB storage
# ═══════════════════════════════════════════════════════════════

async def test_analytics_storage():
    print("\n── 6. Analytics DB storage ──")
    import database as db
    await db.init_db()

    NEG_ID = "m4test001"
    import aiosqlite
    from config import DATABASE_PATH
    async with aiosqlite.connect(DATABASE_PATH) as conn:
        await conn.execute(
            "INSERT OR REPLACE INTO negotiations (id, feature_type, initiator_id) VALUES (?, ?, ?)",
            (NEG_ID, "expenses", 55555)
        )
        await conn.commit()

    timeline = [{"round": 1, "score_a": 80, "score_b": 60},
                {"round": 2, "score_a": 85, "score_b": 75}]
    fairness = 100 - abs(85 - 75)   # = 90

    await db.store_analytics({
        "negotiation_id": NEG_ID,
        "satisfaction_timeline": json.dumps(timeline),
        "concession_log": json.dumps([{"round": 1, "by": "A", "gave_up": "morning slot"}]),
        "fairness_score": fairness,
        "total_concessions_a": 1,
        "total_concessions_b": 0,
    })

    row = await db.get_analytics(NEG_ID)
    record(row is not None, "Analytics row stored in negotiation_analytics table")
    if row:
        row = dict(row)
        record(abs(row["fairness_score"] - 90) < 0.01,
               f"Fairness score stored correctly",
               str(row["fairness_score"]))
        record(row["total_concessions_a"] == 1,
               "total_concessions_a stored correctly",
               str(row["total_concessions_a"]))

    # Cleanup
    async with aiosqlite.connect(DATABASE_PATH) as conn:
        await conn.execute("DELETE FROM negotiation_analytics WHERE negotiation_id = ?", (NEG_ID,))
        await conn.execute("DELETE FROM negotiations WHERE id = ?", (NEG_ID,))
        await conn.commit()


# ═══════════════════════════════════════════════════════════════
# 7. bot.py on_resolution wiring — check source for voice+analytics
# ═══════════════════════════════════════════════════════════════

def test_resolution_wiring():
    print("\n── 7. on_resolution wiring in bot.py ──")
    src = open(os.path.join(BOTS, "bot.py")).read()

    record("generate_voice_summary" in src,
           "bot.py calls generate_voice_summary in on_resolution")
    record("build_voice_text" in src,
           "bot.py calls build_voice_text in on_resolution")
    record("store_analytics" in src,
           "bot.py calls db.store_analytics in on_resolution")
    record("upi_link" in src,
           "bot.py generates UPI link in on_resolution")
    record("send_voice" in src,
           "bot.py sends voice note via send_voice in on_resolution")


# ═══════════════════════════════════════════════════════════════
# 8. Decline-before-get bug fixed
# ═══════════════════════════════════════════════════════════════

def test_decline_bug_fixed():
    print("\n── 8. Decline-before-get bug fix ──")
    src = open(os.path.join(BOTS, "bot.py")).read()

    # After the fix, data = get(...) should come BEFORE pop(...)
    # within the decline branch
    decline_block_start = src.find('if action == "decline":')
    if decline_block_start == -1:
        record(False, "decline branch found in bot.py")
        return
    record(True, "decline branch found in bot.py")

    # Get the slice of text for the decline handler (up to return)
    decline_slice = src[decline_block_start:decline_block_start + 600]
    pos_get = decline_slice.find("pending_coordinations.get(neg_id")
    pos_pop = decline_slice.find("pending_coordinations.pop(neg_id")
    record(pos_get < pos_pop and pos_get != -1 and pos_pop != -1,
           "initiator_id is fetched BEFORE pop() in decline branch",
           f"get@{pos_get} pop@{pos_pop}")


# ═══════════════════════════════════════════════════════════════
# Main runner
# ═══════════════════════════════════════════════════════════════

async def run_all():
    test_imports()
    test_run_py_structure()
    test_voice_module()
    await test_voice_graceful_failure()
    await test_upi_tool()
    await test_analytics_storage()
    test_resolution_wiring()
    test_decline_bug_fixed()

    passed = sum(1 for s, _, _ in results if s == PASS)
    total  = len(results)
    print("\n" + "=" * 60)
    print(f"  MILESTONE 4 TESTS: {passed}/{total} passed")
    print("=" * 60)

    if passed == total:
        print("""
🏆 ALL TESTS PASSED — Milestone 4 is ready!

══ HOW TO RUN (Live Telegram test) ══════════════════════════
  cd e:\\negot8\\backend
  python run.py

══ FULL FLOW TEST (two phones) ══════════════════════════════
  Phone A:  /start
            /personality  → pick Aggressive (😤)
            /coordinate @PhoneB_username
            "Split our hotel bill, I paid ₹8000, john@upi, want 60-40 split"

  Phone B:  /start
            /personality  → pick Empathetic (💚)
            /pending  → tap Accept
            "I think 50-50 is fair, my upi is jane@upi"

  ▶ Watch round-by-round updates appear on BOTH phones
  ▶ Final resolution arrives with 📊 satisfaction scores
  ▶ 🎙 Voice note sent to each user (different voices!)
  ▶ 💳 UPI Pay button appended to expense resolutions
  ▶ Check DB: sqlite3 negot8.db "SELECT * FROM negotiation_analytics;"
""")
    else:
        failed = [(n, d) for s, n, d in results if s == FAIL]
        print(f"\n⚠️  {total - passed} test(s) failed:")
        for name, detail in failed:
            print(f"     • {name}")
            if detail:
                print(f"       {detail}")
        print()

if __name__ == "__main__":
    asyncio.run(run_all())
