# Fashion Stylist Engine — Ralph Loop
# `/fashion-stylist-ralph-loop`

Vòng lặp tự động kiểm tra `StylistEngine` + `fashion_rules.json`:
- **Bước 1**: Chạy script test → in ra report từng anchor product_line
- **Bước 2**: AI Judge đọc report → chấm PASS / FAIL từng tiêu chí  
- **Bước 3**: Nếu FAIL → AI Builder tự sửa `fashion_rules.json` hoặc `stylist_engine.py`
- **Lặp lại** cho đến khi `PASS_EVAL`

## Chuẩn bị (chỉ làm 1 lần)

// turbo
Tạo script test nếu chưa có:

```powershell
# Kiểm tra xem script đã tồn tại chưa
Test-Path "D:\cnf\chatbot-canifa-feedback\backend\scripts\test_stylist_engine.py"
```

## Bước 1 — Tạo test script

// turbo
```powershell
New-Item -ItemType Directory -Force -Path "D:\cnf\chatbot-canifa-feedback\backend\scripts" | Out-Null
```

Tạo file `D:\cnf\chatbot-canifa-feedback\backend\scripts\test_stylist_engine.py`:

```python
"""
Test StylistEngine — kiểm tra fallback rules, gender filter, occasion pairing.
Usage: python scripts/test_stylist_engine.py
"""
import sys, json
sys.path.insert(0, "D:/cnf/chatbot-canifa-feedback/backend")

from worker.stylist_engine import StylistEngine

# ── Danh sách anchor cần test ───────────────────────────────
ANCHORS_TO_TEST = [
    ("Áo Sơ mi",       "nữ"),
    ("Áo Polo",        "nam"),
    ("Áo phông",       "unisex"),
    ("Áo kiểu",        "nữ"),
    ("Blazer",         "nữ"),
    ("Cardigan",       "nữ"),
    ("Áo nỉ có mũ",   "nam"),
    ("Quần jean",      "nữ"),
    ("Quần Khaki",     "nam"),
    ("Quần soóc",      "nữ"),
    ("Chân váy",       "nữ"),
    ("Váy liền",       "nữ"),
    ("Áo khoác gió",   "nam"),
    ("Áo bra active",  "nữ"),
    ("Quần leggings",  "nữ"),
]

# ── Luật tối thiểu: anchor này PHẢI có occasion này trong kết quả ─
REQUIRED_OCCASIONS = {
    "Áo Sơ mi":      ["di_lam", "di_choi"],
    "Áo Polo":       ["di_lam", "hang_ngay"],
    "Áo phông":      ["hang_ngay"],
    "Áo kiểu":       ["di_choi", "di_tiec"],
    "Blazer":        ["di_lam"],
    "Cardigan":      ["hang_ngay"],
    "Áo nỉ có mũ":  ["hang_ngay"],
    "Quần jean":     ["hang_ngay", "di_choi"],
    "Quần Khaki":    ["di_lam", "hang_ngay"],
    "Quần soóc":     ["hang_ngay"],
    "Chân váy":      ["di_lam", "di_choi"],
    "Váy liền":      ["di_choi"],
    "Áo khoác gió":  ["the_thao"],
    "Áo bra active": ["the_thao"],
    "Quần leggings": ["the_thao"],
}

# ── Luật cứng: các kết hợp TUYỆT ĐỐI KHÔNG được phép ────────
FORBIDDEN_PAIRS = [
    # (anchor_pl, target_pl)
    ("Quần mặc nhà", "Áo Sơ mi"),
    ("Áo mặc nhà",   "Quần jean"),
    ("Bộ mặc nhà",   "Chân váy"),
    ("Quần soóc",    "Áo khoác chần bông"),  # mùa hè + đồ đông
    ("Áo bra active","Blazer"),
]

def run_test():
    engine = StylistEngine()
    # Override catalog với mock nhỏ để test fallback rules (không cần DB thật)
    mock_catalog = []
    fallback = engine.rules.get("fallback_occasion_rules", {})
    roles = engine.rules.get("product_line_to_role", {})
    
    # Build mock catalog từ fallback rules
    fake_id = 1000
    for anchor, occ_map in fallback.items():
        for occ, targets in occ_map.items():
            for tgt in targets:
                role = roles.get(tgt, None)
                if not role:
                    continue
                mock_catalog.append({
                    "code": f"MOCK-{fake_id}",
                    "name": f"{tgt} (Mock)",
                    "color": "Trắng",
                    "product_line": tgt,
                    "gender": "unisex",
                    "age_group": "người lớn",
                    "image": "",
                    "occasion_tags": [occ],
                    "style_tags": ["Basic"],
                    "material_tags": ["cotton"],
                    "season_tags": [],
                })
                fake_id += 1

    print(f"Mock catalog: {len(mock_catalog)} items từ fallback rules\n")
    
    results = []
    for anchor_pl, gender in ANCHORS_TO_TEST:
        # Build fake source product
        src_role = roles.get(anchor_pl)
        source = {
            "code": f"ANCHOR-{anchor_pl}",
            "name": f"{anchor_pl} (Anchor)",
            "color": "Đen",
            "product_line": anchor_pl,
            "gender": gender,
            "age_group": "người lớn",
            "image": "",
            "occasion_tags": [],
            "style_tags": ["Basic"],
            "material_tags": ["cotton"],
            "season_tags": [],
        }
        
        full_catalog = [source] + mock_catalog
        
        try:
            matches = engine._compute_matches(source, full_catalog)
        except Exception as e:
            results.append({"anchor": anchor_pl, "status": "ERROR", "detail": str(e), "occasions": {}})
            continue
        
        # Kiểm tra required occasions
        found_occs = set(matches.keys())
        required = set(REQUIRED_OCCASIONS.get(anchor_pl, []))
        missing_occs = required - found_occs
        
        # Kiểm tra forbidden pairs
        violations = []
        for occ, role_data in matches.items():
            for role, items in role_data.items():
                for item in items:
                    tgt_pl = item.get("product_line", "")
                    if (anchor_pl, tgt_pl) in FORBIDDEN_PAIRS:
                        violations.append(f"{anchor_pl} + {tgt_pl} tại {occ}")
        
        total_matches = sum(len(items) for rd in matches.values() for items in rd.values())
        
        if missing_occs or violations:
            status = "FAIL"
        elif total_matches == 0:
            status = "EMPTY"
        else:
            status = "PASS"
        
        results.append({
            "anchor": anchor_pl,
            "gender": gender,
            "status": status,
            "total_matches": total_matches,
            "found_occasions": sorted(found_occs),
            "missing_occasions": sorted(missing_occs),
            "violations": violations,
        })
    
    # ── In report ────────────────────────────────────────────
    print("=" * 65)
    print("  FASHION STYLIST ENGINE — TEST REPORT")
    print("=" * 65)
    
    pass_count = fail_count = empty_count = error_count = 0
    for r in results:
        s = r["status"]
        icon = {"PASS": "✅", "FAIL": "❌", "EMPTY": "⚠️", "ERROR": "💥"}.get(s, "?")
        print(f"\n{icon} [{s}] {r['anchor']} ({r.get('gender','')})")
        if s == "PASS":
            occ_str = ", ".join(r.get("found_occasions", []))
            print(f"   Occasions: {occ_str} | Matches: {r.get('total_matches',0)}")
            pass_count += 1
        elif s == "FAIL":
            if r.get("missing_occasions"):
                print(f"   ❌ Thiếu occasion: {r['missing_occasions']}")
            if r.get("violations"):
                for v in r["violations"]:
                    print(f"   ❌ Vi phạm forbidden pair: {v}")
            fail_count += 1
        elif s == "EMPTY":
            print(f"   ⚠️ Không có match nào — kiểm tra fallback_occasion_rules")
            empty_count += 1
        elif s == "ERROR":
            print(f"   💥 Lỗi: {r['detail']}")
            error_count += 1
    
    print("\n" + "=" * 65)
    total = len(results)
    print(f"  KẾT QUẢ: {pass_count}/{total} PASS | {fail_count} FAIL | {empty_count} EMPTY | {error_count} ERROR")
    print("=" * 65)
    
    if fail_count == 0 and empty_count == 0 and error_count == 0:
        print("\nPASS_EVAL")
    else:
        print(f"\nFAIL_EVAL: {fail_count} FAIL, {empty_count} EMPTY, {error_count} ERROR")

if __name__ == "__main__":
    run_test()
```

## Bước 2 — Chạy test lần đầu

// turbo
```powershell
$env:PYTHONPATH = "D:\cnf\chatbot-canifa-feedback\backend"
Set-Location "D:\cnf\chatbot-canifa-feedback\backend"
.\.venv\Scripts\python.exe scripts\test_stylist_engine.py 2>&1 | Tee-Object -FilePath "plan\tmp_fashion_test.txt"
Get-Content "plan\tmp_fashion_test.txt"
```

## Bước 3 — Ralph Loop (tự động sửa nếu FAIL)

Chạy vòng lặp đến khi PASS_EVAL:

```powershell
$ITER = 1
while ($true) {
    Write-Host "=== VONG $ITER ===" -ForegroundColor Cyan
    
    # Run test
    $env:PYTHONPATH = "D:\cnf\chatbot-canifa-feedback\backend"
    Set-Location "D:\cnf\chatbot-canifa-feedback\backend"
    .\.venv\Scripts\python.exe scripts\test_stylist_engine.py 2>&1 | Out-File -Encoding utf8 "plan\tmp_fashion_test.txt"
    $output = Get-Content "plan\tmp_fashion_test.txt" -Raw
    Write-Host $output
    
    if ($output -match "PASS_EVAL") {
        Write-Host ">>> PASS! DONE." -ForegroundColor Green
        break
    }
    
    # Gọi Gemini sửa
    $prompt = "@plan\tmp_fashion_test.txt @worker\stylist_engine.py @worker\fashion_rules.json " +
              "Doc loi trong file test. Sua worker/stylist_engine.py hoac worker/fashion_rules.json (fallback_occasion_rules) " +
              "de fix loi. Chi sua dung chỗ, khong xoa code cu."
    gemini --yolo -p $prompt
    
    $ITER++
    Start-Sleep -Seconds 5
}
```

## Tiêu chí đánh giá

| Tiêu chí | Pass condition |
|---|---|
| Tất cả anchor có ≥ 1 match | `total_matches > 0` |
| Required occasions có đủ | `missing_occasions = []` |
| Không vi phạm forbidden pairs | `violations = []` |
| Không có lỗi runtime | `status != ERROR` |

## Ghi chú

- Script dùng **mock catalog** từ `fallback_occasion_rules` → không cần kết nối DB thật
- Nếu muốn test với catalog thật từ DB: bỏ mock, dùng `engine._get_catalog()`
- Sau khi engine PASS, chạy `run_fashion_ralph.ps1` để test với data thật từ StarRocks
