"""
🧪 Test Suite: Outfit Recommendations & Similar Items Flow
============================================================
Tests 3 chiến thuật chính:
  1. Similar Items (Tự động gợi ý khi giới thiệu SP)
  2. Outfit Recommendations (Chỉ khi khách hỏi phối đồ)
  3. Pivoting (Chuyển gốc liên tục qua nhiều lượt chat)

Usage:
  py tests/test_outfit_similar_flow.py
  py tests/test_outfit_similar_flow.py --base-url http://localhost:5001
"""

import argparse
import asyncio
import json
import logging
import sys
import time
from dataclasses import dataclass, field
from typing import Optional

import httpx

# ============================================================
# CONFIG
# ============================================================
DEFAULT_BASE_URL = "http://localhost:5001"
CHAT_ENDPOINT = "/api/agent/chat-dev"
TIMEOUT_SECONDS = 120  # LLM can be slow

# Force UTF-8 output on Windows
if sys.platform == "win32":
    import io
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger(__name__)


@dataclass
class TestResult:
    name: str
    query: str
    passed: bool
    ai_response: str = ""
    product_ids: list = field(default_factory=list)
    duration_sec: float = 0.0
    error: Optional[str] = None
    notes: str = ""


# ============================================================
# TEST SCENARIOS
# ============================================================
# Each scenario is a dict with:
#   name: Human-readable test name
#   query: The user query to send
#   expect: What to check in the response
#     - has_products: bool (expect product_ids non-empty)
#     - min_products: int (minimum product count)
#     - response_contains: list[str] (keywords in ai_response)
#     - response_not_contains: list[str] (anti-keywords)

TEST_SCENARIOS = [
    # =========================================================
    # GROUP 1: Basic Product Search (Baseline — No outfit logic)
    # =========================================================
    {
        "name": "G1.01 — Tìm áo phông nam cơ bản",
        "query": "Tìm áo phông nam đi chơi",
        "expect": {"has_products": True, "min_products": 1},
    },
    {
        "name": "G1.02 — Tìm quần cho bé gái",
        "query": "Quần cho bé gái 6 tuổi",
        "expect": {"has_products": True, "min_products": 1},
    },
    {
        "name": "G1.03 — Tìm váy nữ đi tiệc",
        "query": "Tìm váy đi tiệc cho nữ",
        "expect": {"has_products": True, "min_products": 1},
    },

    # =========================================================
    # GROUP 2: Similar Items (AI nên tự động gợi ý thêm)
    # =========================================================
    {
        "name": "G2.01 — Tìm bằng SKU → expect similar mention",
        "query": "Cho xem mẫu 1BP24C002",
        "expect": {"has_products": True},
    },
    {
        "name": "G2.02 — Tìm tất trẻ em → expect similar items",
        "query": "Tìm tất cho bé",
        "expect": {"has_products": True, "min_products": 1},
    },
    {
        "name": "G2.03 — Có mẫu áo tương tự không?",
        "query": "Có mẫu áo phông nào tương tự mẫu 3TS26S018-SR079 không?",
        "expect": {"has_products": True},
    },

    # =========================================================
    # GROUP 3: Outfit Recommendations (CHỈ khi hỏi phối đồ)
    # =========================================================
    {
        "name": "G3.01 — Hỏi phối quần cho áo",
        "query": "Áo phông 3TS26S018-SR079 phối với quần gì cho bé?",
        "expect": {"has_products": True},
    },
    {
        "name": "G3.02 — Hỏi phối áo cho quần",
        "query": "Quần nỉ 1BP24C002 mặc với áo gì đẹp?",
        "expect": {"has_products": True},
    },
    {
        "name": "G3.03 — Hỏi combo/set outfit",
        "query": "Gợi ý nguyên bộ cho bé gái đi học",
        "expect": {"has_products": True, "min_products": 1},
    },
    {
        "name": "G3.04 — Hỏi phụ kiện phối",
        "query": "Phối phụ kiện gì cho mẫu 1BS25S008-SK010?",
        "expect": {"has_products": True},
    },

    # =========================================================
    # GROUP 4: Cross-sell / Upsell tự nhiên
    # =========================================================
    {
        "name": "G4.01 — Khách chốt 1 món → AI upsell tự nhiên",
        "query": "Ok lấy cái quần 1BP24C002 đi, còn gì hay không?",
        "expect": {"has_products": True},
    },
    {
        "name": "G4.02 — Hỏi thêm combo tiết kiệm",
        "query": "Mua thêm gì nữa để được giảm giá?",
        "expect": {"has_products": False},  # Expect promotions redirect, not products
    },

    # =========================================================
    # GROUP 5: Phối đồ theo dịp (AI tự suy luận)
    # =========================================================
    {
        "name": "G5.01 — Đồ đi biển cho nam",
        "query": "Đồ đi biển cho nam, thoáng mát",
        "expect": {"has_products": True, "min_products": 1},
    },
    {
        "name": "G5.02 — Đồ đi làm cho nữ",
        "query": "Gợi ý đồ đi làm công sở cho nữ 28 tuổi",
        "expect": {"has_products": True, "min_products": 1},
    },
    {
        "name": "G5.03 — Đồ đi chơi cuối tuần",
        "query": "Cuối tuần đi chơi với bạn gái mặc gì cho đẹp?",
        "expect": {"has_products": True, "min_products": 1},
    },

    # =========================================================
    # GROUP 6: Edge Cases & Negative Tests
    # =========================================================
    {
        "name": "G6.01 — Chào hỏi (không gọi tool)",
        "query": "Xin chào shop",
        "expect": {"has_products": False},
    },
    {
        "name": "G6.02 — Hỏi chính sách (redirect knowledge)",
        "query": "Chính sách đổi trả như thế nào?",
        "expect": {"has_products": False},
    },
    {
        "name": "G6.03 — Hỏi cửa hàng",
        "query": "Có cửa hàng Canifa ở Hải Phòng không?",
        "expect": {"has_products": False},
    },
    {
        "name": "G6.04 — SKU không tồn tại",
        "query": "Cho xem mẫu ZZZZ99999",
        "expect": {"has_products": False},
    },

    # =========================================================
    # GROUP 7: Multi-turn Pivoting Simulation
    # =========================================================
    {
        "name": "G7.01 — Lượt 1: Tìm áo gốc",
        "query": "Tìm áo phông cho bé trai",
        "expect": {"has_products": True, "min_products": 1},
    },
    {
        "name": "G7.02 — Lượt 2: Hỏi phối quần (pivot)",
        "query": "Áo đó phối quần gì cho bé đi học?",
        "expect": {"has_products": True},
    },
    {
        "name": "G7.03 — Lượt 3: Hỏi SP tương tự (pivot tiếp)",
        "query": "Có quần nào form rộng hơn không?",
        "expect": {"has_products": True},
    },

    # =========================================================
    # GROUP 8: Price-sensitive Queries
    # =========================================================
    {
        "name": "G8.01 — Tìm theo giá",
        "query": "Áo phông nam dưới 200k",
        "expect": {"has_products": True},
    },
    {
        "name": "G8.02 — Khách nói đắt",
        "query": "Đắt quá, có mẫu nào rẻ hơn tương tự không?",
        "expect": {"has_products": True},
    },

    # =========================================================
    # GROUP 9: Size & Stock Queries
    # =========================================================
    {
        "name": "G9.01 — Hỏi size có sẵn",
        "query": "Mẫu 3TS26S018-SR079 có size gì?",
        "expect": {"has_products": True},
    },
    {
        "name": "G9.02 — Tư vấn size theo cân nặng",
        "query": "60kg cao 1m65 mặc size gì?",
        "expect": {"has_products": False},
    },

    # =========================================================
    # GROUP 10: Stress / Combo Queries
    # =========================================================
    {
        "name": "G10.01 — Mua cho cả nhà",
        "query": "Tư vấn đồ cho gia đình 4 người: bố mẹ và 2 con nhỏ, budget 2 triệu",
        "expect": {"has_products": True, "min_products": 1},
    },
    {
        "name": "G10.02 — Hỏi nhiều mã cùng lúc",
        "query": "So sánh 3TS26S018-SR079 và 1BP24C002 cái nào đáng mua hơn?",
        "expect": {"has_products": True, "min_products": 1},
    },
]


# ============================================================
# TEST RUNNER
# ============================================================
async def run_single_test(
    client: httpx.AsyncClient,
    scenario: dict,
    base_url: str,
) -> TestResult:
    name = scenario["name"]
    query = scenario["query"]
    expect = scenario.get("expect", {})

    result = TestResult(name=name, query=query, passed=False)

    try:
        start = time.time()
        resp = await client.post(
            f"{base_url}{CHAT_ENDPOINT}",
            json={"user_query": query},
            timeout=TIMEOUT_SECONDS,
        )
        result.duration_sec = round(time.time() - start, 2)

        if resp.status_code != 200:
            result.error = f"HTTP {resp.status_code}: {resp.text[:200]}"
            return result

        data = resp.json()
        result.ai_response = data.get("ai_response", "")[:500]
        result.product_ids = data.get("product_ids", [])

        # Evaluate expectations
        passed = True
        notes_parts = []

        if "has_products" in expect:
            has = len(result.product_ids) > 0
            if has != expect["has_products"]:
                passed = False
                notes_parts.append(
                    f"products: expected {'yes' if expect['has_products'] else 'no'}, got {'yes' if has else 'no'} ({len(result.product_ids)})"
                )
            else:
                notes_parts.append(f"products: {len(result.product_ids)} ✓")

        if "min_products" in expect:
            if len(result.product_ids) < expect["min_products"]:
                passed = False
                notes_parts.append(
                    f"min_products: expected >={expect['min_products']}, got {len(result.product_ids)}"
                )

        if "response_contains" in expect:
            for keyword in expect["response_contains"]:
                if keyword.lower() not in result.ai_response.lower():
                    passed = False
                    notes_parts.append(f"missing keyword: '{keyword}'")

        if "response_not_contains" in expect:
            for keyword in expect["response_not_contains"]:
                if keyword.lower() in result.ai_response.lower():
                    passed = False
                    notes_parts.append(f"unwanted keyword found: '{keyword}'")

        result.passed = passed
        result.notes = " | ".join(notes_parts)

    except httpx.TimeoutException:
        result.error = f"Timeout after {TIMEOUT_SECONDS}s"
    except httpx.ConnectError:
        result.error = "Connection refused — is the server running?"
    except Exception as e:
        result.error = str(e)[:200]

    return result


async def run_all_tests(base_url: str, sequential: bool = True):
    logger.info(f"🚀 Starting test suite against {base_url}")
    logger.info(f"📋 Total scenarios: {len(TEST_SCENARIOS)}")
    logger.info("=" * 70)

    results: list[TestResult] = []

    async with httpx.AsyncClient() as client:
        # Quick health check
        try:
            health = await client.get(f"{base_url}/health", timeout=5)
            if health.status_code != 200:
                logger.error(f"❌ Health check failed: {health.status_code}")
                return
            logger.info("✅ Health check passed\n")
        except Exception as e:
            logger.error(f"❌ Cannot connect to {base_url}: {e}")
            logger.error("   Make sure the server is running!")
            return

        for i, scenario in enumerate(TEST_SCENARIOS, 1):
            logger.info(f"[{i:02d}/{len(TEST_SCENARIOS)}] {scenario['name']}")
            logger.info(f"   Query: \"{scenario['query']}\"")

            result = await run_single_test(client, scenario, base_url)

            icon = "✅" if result.passed else ("⚠️" if result.error else "❌")
            logger.info(f"   {icon} {'PASS' if result.passed else 'FAIL'} ({result.duration_sec}s)")
            if result.error:
                logger.info(f"   Error: {result.error}")
            if result.notes:
                logger.info(f"   Notes: {result.notes}")
            if result.product_ids:
                logger.info(f"   Products: {result.product_ids[:5]}{'...' if len(result.product_ids) > 5 else ''}")
            logger.info(f"   Response: {result.ai_response[:150]}...")
            logger.info("")

            results.append(result)

            # Small delay between tests to avoid hammering
            if sequential and i < len(TEST_SCENARIOS):
                await asyncio.sleep(1)

    # Summary
    logger.info("=" * 70)
    logger.info("📊 TEST SUMMARY")
    logger.info("=" * 70)

    passed = sum(1 for r in results if r.passed)
    failed = sum(1 for r in results if not r.passed and not r.error)
    errored = sum(1 for r in results if r.error)
    total_time = sum(r.duration_sec for r in results)

    logger.info(f"   ✅ Passed:  {passed}/{len(results)}")
    logger.info(f"   ❌ Failed:  {failed}/{len(results)}")
    logger.info(f"   ⚠️  Errors:  {errored}/{len(results)}")
    logger.info(f"   ⏱️  Total:   {total_time:.1f}s")
    logger.info(f"   ⏱️  Avg:     {total_time/len(results):.1f}s per test")

    if failed + errored > 0:
        logger.info("\n❌ FAILURES:")
        for r in results:
            if not r.passed:
                logger.info(f"   • {r.name}: {r.error or r.notes}")

    # Save results to JSON
    output_path = "tests/test_results_outfit_flow.json"
    output_data = {
        "summary": {
            "total": len(results),
            "passed": passed,
            "failed": failed,
            "errored": errored,
            "total_time_sec": round(total_time, 1),
        },
        "results": [
            {
                "name": r.name,
                "query": r.query,
                "passed": r.passed,
                "duration_sec": r.duration_sec,
                "product_count": len(r.product_ids),
                "product_ids": r.product_ids[:10],
                "error": r.error,
                "notes": r.notes,
                "ai_response_preview": r.ai_response[:300],
            }
            for r in results
        ],
    }

    try:
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(output_data, f, ensure_ascii=False, indent=2)
        logger.info(f"\n📄 Results saved to {output_path}")
    except Exception as e:
        logger.warning(f"Could not save results: {e}")


def main():
    parser = argparse.ArgumentParser(description="Test Outfit & Similar Items Flow")
    parser.add_argument(
        "--base-url",
        default=DEFAULT_BASE_URL,
        help=f"Base URL of the chatbot API (default: {DEFAULT_BASE_URL})",
    )
    parser.add_argument(
        "--parallel",
        action="store_true",
        help="Run tests in parallel (faster but may cause issues)",
    )
    args = parser.parse_args()

    asyncio.run(run_all_tests(args.base_url, sequential=not args.parallel))


if __name__ == "__main__":
    main()
