#!/usr/bin/env python3
"""
test_voice_widget_agent.py
==========================
Browser agent that tests the Talking Widget voice system end-to-end.
Uses Chrome's fake audio capture to inject WAV files as a virtual microphone —
no real microphone hardware required.

Key Chrome flags for fake microphone:
  --use-fake-ui-for-media-stream        -- auto-grant microphone permissions
  --use-fake-device-for-media-stream    -- use virtual audio device
  --use-file-for-fake-audio-capture=X  -- inject WAV file as mic input

Test Scenarios:
  1. New Visitor Test      -- First call, no memory, fresh greeting
  2. Returning Visitor Test -- Second call, memory recalled from previous session
  3. Memory Persistence Test -- Verify context injected into Telnyx before call

Usage:
  # First generate test audio:
  python3 scripts/generate_test_audio.py

  # Then run tests:
  python3 scripts/test_voice_widget_agent.py

  # Run specific scenario:
  python3 scripts/test_voice_widget_agent.py --scenario new_visitor
  python3 scripts/test_voice_widget_agent.py --scenario returning_visitor
  python3 scripts/test_voice_widget_agent.py --scenario memory_check

Requirements:
  pip3 install playwright requests
  playwright install chromium
"""

import os
import sys
import json
import time
import argparse
import logging
import requests
import uuid
from datetime import datetime

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%H:%M:%S",
)
logger = logging.getLogger("widget_tester")

WIDGET_URL = os.environ.get("WIDGET_URL", "https://talkingwidget.ai")
API_BASE = os.environ.get("API_BASE", "https://api.sunaivadigital.com")
AUDIO_DIR = "/tmp/tw_test_audio"
CALL_TIMEOUT_MS = 45_000   # 45s max wait for call to connect
RESPONSE_WAIT_MS = 30_000  # 30s to wait for AI to respond


# ============================================================================
# Helper: API checks
# ============================================================================

def check_visitor_context(visitor_id: str) -> dict:
    """Verify the memory API has context for this visitor before calling."""
    try:
        resp = requests.get(
            f"{API_BASE}/api/widget/context/{visitor_id}",
            timeout=5,
        )
        if resp.ok:
            return resp.json()
        return {}
    except Exception as e:
        logger.warning(f"Context check failed: {e}")
        return {}


def identify_visitor(visitor_id: str, domain: str = "talkingwidget.ai") -> dict:
    """Call the identify endpoint and verify context is injected."""
    try:
        resp = requests.post(
            f"{API_BASE}/api/widget/identify",
            json={"visitor_id": visitor_id, "site_domain": domain},
            timeout=8,
        )
        if resp.ok:
            return resp.json()
        logger.warning(f"identify failed: {resp.status_code}")
        return {}
    except Exception as e:
        logger.error(f"identify error: {e}")
        return {}


# ============================================================================
# Browser Agent
# ============================================================================

def launch_browser_with_fake_mic(audio_file: str, headless: bool = False):
    """Launch Chromium with fake microphone pointing to a WAV file."""
    try:
        from playwright.sync_api import sync_playwright
    except ImportError:
        logger.error("Playwright not installed: pip3 install playwright && playwright install chromium")
        sys.exit(1)

    if not os.path.exists(audio_file):
        logger.error(f"Audio file not found: {audio_file}")
        logger.error("Generate test audio first: python3 scripts/generate_test_audio.py")
        sys.exit(1)

    p = sync_playwright().start()

    browser = p.chromium.launch(
        headless=headless,
        args=[
            "--use-fake-ui-for-media-stream",           # Auto-grant mic permission
            "--use-fake-device-for-media-stream",        # Use virtual audio device
            f"--use-file-for-fake-audio-capture={audio_file}",  # WAV as mic input
            "--autoplay-policy=no-user-gesture-required",
            "--disable-web-security",                    # Allow cross-origin for testing
            "--no-sandbox",
        ],
    )

    context = browser.new_context(
        permissions=["microphone"],
        # Appear as a real desktop browser
        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                   "(KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
    )

    return p, browser, context


def run_new_visitor_test(headless: bool = False) -> dict:
    """
    Test 1: New visitor first call.
    Verifies:
    - Widget loads successfully
    - Microphone permission auto-granted
    - Telnyx call initiates
    - AI responds (any response = pass)
    - visitor_profiles row created in DB
    """
    logger.info("=" * 60)
    logger.info("TEST 1: New Visitor — First Call")
    logger.info("=" * 60)

    audio_file = os.path.join(AUDIO_DIR, "q1_first_visit.wav")
    visitor_id = str(uuid.uuid4())  # Fresh UUID = new visitor
    results = {"test": "new_visitor", "visitor_id": visitor_id, "passed": False, "steps": []}

    # Step 1: Verify API is reachable
    try:
        resp = requests.get(f"{API_BASE}/api/health", timeout=5)
        if resp.ok:
            results["steps"].append({"step": "api_health", "status": "pass"})
            logger.info("✅ API health check passed")
        else:
            results["steps"].append({"step": "api_health", "status": "fail", "detail": resp.status_code})
            logger.warning(f"⚠️  API health returned {resp.status_code}")
    except Exception as e:
        results["steps"].append({"step": "api_health", "status": "error", "detail": str(e)})
        logger.warning(f"⚠️  API not reachable: {e}")

    # Step 2: Pre-identify visitor (simulates widget JS call)
    identity = identify_visitor(visitor_id)
    if identity.get("ready"):
        results["steps"].append({"step": "identify", "status": "pass", "data": identity})
        logger.info(f"✅ Visitor identified: session_count={identity.get('session_count', 0)}")
    else:
        results["steps"].append({"step": "identify", "status": "fail"})
        logger.warning("⚠️  Visitor identify failed — proceeding anyway")

    # Step 3: Load widget page with fake microphone
    p, browser, context = launch_browser_with_fake_mic(audio_file, headless=headless)

    try:
        page = context.new_page()

        # Inject visitor_id into localStorage before page load
        page.add_init_script(f"""
            window.localStorage.setItem('tw_visitor_id', '{visitor_id}');
        """)

        logger.info(f"Loading: {WIDGET_URL}")
        page.goto(WIDGET_URL, wait_until="networkidle", timeout=30_000)
        results["steps"].append({"step": "page_load", "status": "pass"})
        logger.info("✅ Page loaded")

        # Step 4: Find and click the avatar/FAB button
        time.sleep(2)  # Let JS initialise

        # Try avatar tap button first, then FAB
        clicked = False
        for selector in [
            "#avatar-idle-overlay",
            ".avatar-idle-overlay",
            "#voice-fab",
            ".voice-widget-fab",
            "[onclick*='heroTriggerCall']",
        ]:
            try:
                el = page.query_selector(selector)
                if el and el.is_visible():
                    el.click()
                    clicked = True
                    logger.info(f"✅ Clicked: {selector}")
                    break
            except Exception:
                continue

        if not clicked:
            # JavaScript trigger as last resort
            page.evaluate("if(typeof heroTriggerCall === 'function') heroTriggerCall(); else if(typeof triggerVoiceWidget === 'function') triggerVoiceWidget();")
            clicked = True
            logger.info("✅ Triggered via JavaScript")

        results["steps"].append({"step": "widget_click", "status": "pass" if clicked else "fail"})

        # Step 5: Wait for Telnyx call to establish
        logger.info("Waiting for call to connect...")
        call_connected = False

        for attempt in range(15):
            time.sleep(2)
            # Check for call-active indicators
            call_indicators = [
                ".avatar-active-overlay",  # Our custom active state
                "[call-status='active']",
                "[state='active']",
                "[connected='true']",
            ]
            for selector in call_indicators:
                try:
                    el = page.query_selector(selector)
                    if el:
                        style = el.get_attribute("style") or ""
                        display = page.evaluate(f"getComputedStyle(document.querySelector('{selector}')).display") if el else "none"
                        if display != "none":
                            call_connected = True
                            break
                except Exception:
                    continue

            # Also check if avatar is in in-call state
            try:
                stage = page.query_selector("#avatar-stage")
                if stage and "in-call" in (stage.get_attribute("class") or ""):
                    call_connected = True
            except Exception:
                pass

            if call_connected:
                logger.info(f"✅ Call connected after {attempt * 2}s")
                break

        results["steps"].append({
            "step": "call_connect",
            "status": "pass" if call_connected else "warn",
            "detail": "Connected" if call_connected else "No explicit call-active indicator found"
        })

        # Step 6: Wait for audio injection to play
        logger.info("Waiting for fake audio to inject...")
        time.sleep(5)

        # Step 7: Capture any console/network events as evidence
        console_messages = []
        page.on("console", lambda msg: console_messages.append(msg.text))
        time.sleep(3)

        results["steps"].append({"step": "audio_injection", "status": "pass"})
        results["steps"].append({"step": "console_captured", "messages": console_messages[:10]})

        logger.info("✅ Test 1 sequence complete")
        results["passed"] = True

    except Exception as e:
        logger.error(f"Test 1 error: {e}")
        results["error"] = str(e)
    finally:
        try:
            browser.close()
            p.stop()
        except Exception:
            pass

    return results


def run_returning_visitor_test(visitor_id: str = None, headless: bool = False) -> dict:
    """
    Test 2: Returning visitor — verifies memory recall.
    Requires at least 1 previous session in the DB.

    Verifies:
    - visitor_profiles session_count > 0
    - context endpoint returns past session data
    - Telnyx assistant context is injected before call
    """
    logger.info("=" * 60)
    logger.info("TEST 2: Returning Visitor — Memory Recall")
    logger.info("=" * 60)

    if not visitor_id:
        # Try to use existing visitor from localStorage simulation
        visitor_id = "test-returning-visitor-" + str(uuid.uuid4())[:8]

    audio_file = os.path.join(AUDIO_DIR, "q5_memory_test.wav")
    results = {"test": "returning_visitor", "visitor_id": visitor_id, "passed": False, "steps": []}

    # Step 1: Check API for visitor context
    context_data = check_visitor_context(visitor_id)
    session_count = context_data.get("session_count", 0)

    if session_count > 0:
        results["steps"].append({
            "step": "context_check",
            "status": "pass",
            "session_count": session_count,
            "context_preview": context_data.get("context", "")[:200],
        })
        logger.info(f"✅ Found {session_count} previous sessions for visitor {visitor_id[:12]}...")
    else:
        results["steps"].append({
            "step": "context_check",
            "status": "warn",
            "detail": "No previous sessions found — run new_visitor test first",
        })
        logger.warning(f"⚠️  No previous sessions for visitor {visitor_id[:12]}...")
        logger.warning("    Run --scenario new_visitor first to create session history")

    # Step 2: Pre-identify (inject context)
    identity = identify_visitor(visitor_id)
    if identity.get("ready"):
        is_returning = identity.get("is_returning", False)
        results["steps"].append({
            "step": "identify",
            "status": "pass",
            "is_returning": is_returning,
            "session_count": identity.get("session_count", 0),
        })
        logger.info(f"✅ Visitor identified: is_returning={is_returning}, sessions={identity.get('session_count', 0)}")
    else:
        results["steps"].append({"step": "identify", "status": "fail"})

    # Step 3: Launch browser + load widget
    if not os.path.exists(audio_file):
        audio_file = os.path.join(AUDIO_DIR, "q1_first_visit.wav")

    p, browser, context = launch_browser_with_fake_mic(audio_file, headless=headless)

    try:
        page = context.new_page()
        page.add_init_script(f"""
            window.localStorage.setItem('tw_visitor_id', '{visitor_id}');
        """)

        page.goto(WIDGET_URL, wait_until="networkidle", timeout=30_000)
        logger.info("✅ Widget page loaded")
        time.sleep(2)

        # Trigger call
        page.evaluate("if(typeof heroTriggerCall === 'function') heroTriggerCall(); else if(typeof triggerVoiceWidget === 'function') triggerVoiceWidget();")
        logger.info("✅ Call triggered")
        time.sleep(8)

        results["steps"].append({"step": "call_triggered", "status": "pass"})
        results["passed"] = True

    except Exception as e:
        logger.error(f"Test 2 error: {e}")
        results["error"] = str(e)
    finally:
        try:
            browser.close()
            p.stop()
        except Exception:
            pass

    return results


def run_memory_check_test() -> dict:
    """
    Test 3: API-only memory persistence verification.
    No browser needed — verifies the database layer works correctly.

    Creates a test visitor, runs two simulated sessions,
    verifies context builds correctly between sessions.
    """
    logger.info("=" * 60)
    logger.info("TEST 3: Memory Persistence — API Verification")
    logger.info("=" * 60)

    test_visitor_id = "test-memory-check-" + str(uuid.uuid4())[:8]
    results = {"test": "memory_check", "visitor_id": test_visitor_id, "passed": False, "steps": []}

    # Step 1: First identify (new visitor)
    identity1 = identify_visitor(test_visitor_id)
    results["steps"].append({
        "step": "first_identify",
        "status": "pass" if identity1.get("ready") else "fail",
        "session_count": identity1.get("session_count", -1),
        "is_returning": identity1.get("is_returning", None),
    })
    logger.info(f"✅ First identify: is_returning={identity1.get('is_returning')}, sessions={identity1.get('session_count')}")

    # Step 2: Check initial context (should be "new visitor")
    ctx1 = check_visitor_context(test_visitor_id)
    context_text = ctx1.get("context", "")
    is_new_visitor_context = "New visitor" in context_text or "no previous" in context_text.lower()
    results["steps"].append({
        "step": "initial_context",
        "status": "pass" if is_new_visitor_context else "warn",
        "context_preview": context_text[:200],
    })
    if is_new_visitor_context:
        logger.info("✅ New visitor context correct")
    else:
        logger.warning(f"⚠️  Unexpected initial context: {context_text[:100]}")

    # Step 3: Simulate a call via webhook (create session directly via API)
    test_call_id = "test-call-" + str(uuid.uuid4())[:8]

    # Simulate call.initiated
    webhook_start = requests.post(
        f"{API_BASE}/api/webhook/telnyx",
        json={
            "data": {
                "event_type": "call.initiated",
                "payload": {
                    "call_session_id": test_call_id,
                    "conversation_id": test_visitor_id,
                }
            }
        },
        timeout=8,
    )
    results["steps"].append({
        "step": "simulate_call_start",
        "status": "pass" if webhook_start.ok else "fail",
        "http_status": webhook_start.status_code,
    })
    logger.info(f"✅ Simulated call start: {webhook_start.status_code}")

    time.sleep(1)  # Let background task run

    # Simulate ai.session.end with transcript
    webhook_end = requests.post(
        f"{API_BASE}/api/webhook/telnyx",
        json={
            "data": {
                "event_type": "ai.session.end",
                "payload": {
                    "call_session_id": test_call_id,
                    "duration_ms": 45000,
                    "messages": [
                        {"role": "user", "content": "Hi, I'm interested in adding a voice widget to my website."},
                        {"role": "assistant", "content": "Great! Talking Widget can add an AI voice assistant to any website in minutes."},
                        {"role": "user", "content": "How much does it cost?"},
                        {"role": "assistant", "content": "Plans start at $497 AUD per month for up to 500 minutes."},
                    ]
                }
            }
        },
        timeout=8,
    )
    results["steps"].append({
        "step": "simulate_call_end",
        "status": "pass" if webhook_end.ok else "fail",
        "http_status": webhook_end.status_code,
    })
    logger.info(f"✅ Simulated call end: {webhook_end.status_code}")

    time.sleep(2)  # Wait for background DB write

    # Step 4: Second identify — should now show returning visitor
    identity2 = identify_visitor(test_visitor_id)
    results["steps"].append({
        "step": "second_identify",
        "status": "pass" if identity2.get("session_count", 0) > 0 else "fail",
        "session_count": identity2.get("session_count", -1),
        "is_returning": identity2.get("is_returning", None),
    })
    logger.info(f"✅ Second identify: is_returning={identity2.get('is_returning')}, sessions={identity2.get('session_count')}")

    # Step 5: Verify context includes past session
    ctx2 = check_visitor_context(test_visitor_id)
    context_text2 = ctx2.get("context", "")
    has_memory = (
        "RETURNING VISITOR" in context_text2
        or "called" in context_text2.lower()
        or "Previous Call" in context_text2
    )

    results["steps"].append({
        "step": "memory_context_check",
        "status": "pass" if has_memory else "fail",
        "context_preview": context_text2[:400],
    })

    if has_memory:
        logger.info("✅ Memory context correctly contains previous session data")
        logger.info(f"   Context preview: {context_text2[:200]}")
    else:
        logger.error("❌ Memory context missing — DB write may have failed")
        logger.error(f"   Context text: {context_text2[:200]}")

    results["passed"] = has_memory
    return results


# ============================================================================
# Main
# ============================================================================

def print_result(result: dict):
    """Pretty-print test result."""
    status = "✅ PASSED" if result.get("passed") else "❌ FAILED"
    print(f"\n{status}: {result['test']}")
    for step in result.get("steps", []):
        step_status = "✅" if step.get("status") in ("pass", "warn") else "❌"
        print(f"  {step_status} {step['step']}: {step.get('status', '?')}")
        if "detail" in step:
            print(f"     → {step['detail']}")
        if "context_preview" in step and step.get("context_preview"):
            print(f"     → Context: {step['context_preview'][:150]}...")
    if result.get("error"):
        print(f"  ERROR: {result['error']}")


def main():
    parser = argparse.ArgumentParser(description="Test Talking Widget voice + memory system")
    parser.add_argument(
        "--scenario",
        choices=["new_visitor", "returning_visitor", "memory_check", "all"],
        default="memory_check",
        help="Which test scenario to run (default: memory_check — API only, no browser needed)",
    )
    parser.add_argument(
        "--visitor-id",
        default=None,
        help="Visitor ID for returning_visitor test",
    )
    parser.add_argument(
        "--headless",
        action="store_true",
        help="Run browser tests in headless mode",
    )
    parser.add_argument(
        "--widget-url",
        default=WIDGET_URL,
        help=f"Widget URL to test (default: {WIDGET_URL})",
    )
    args = parser.parse_args()

    widget_url = args.widget_url

    print(f"\n{'=' * 60}")
    print(f"  TALKING WIDGET VOICE + MEMORY TEST SUITE")
    print(f"  Widget: {WIDGET_URL}")
    print(f"  API:    {API_BASE}")
    print(f"  Scenario: {args.scenario}")
    print(f"{'=' * 60}\n")

    results = []

    if args.scenario in ("memory_check", "all"):
        r = run_memory_check_test()
        results.append(r)

    if args.scenario in ("new_visitor", "all"):
        r = run_new_visitor_test(headless=args.headless)
        results.append(r)

    if args.scenario in ("returning_visitor", "all"):
        r = run_returning_visitor_test(
            visitor_id=args.visitor_id,
            headless=args.headless,
        )
        results.append(r)

    print(f"\n{'=' * 60}")
    print("  RESULTS SUMMARY")
    print(f"{'=' * 60}")

    for r in results:
        print_result(r)

    passed = sum(1 for r in results if r.get("passed"))
    total = len(results)
    print(f"\n{passed}/{total} tests passed")

    if passed == total:
        print("✅ All tests passed — memory system is working!")
        sys.exit(0)
    else:
        print("❌ Some tests failed — check logs above")
        sys.exit(1)


if __name__ == "__main__":
    main()
