"""Run dataset_none_detect.py against synthetic + two HF datasets; log to tests/logs/none_detect_results.log."""

from __future__ import annotations

import json
import sys
import traceback
from datetime import datetime
from io import StringIO
from pathlib import Path

# Import dataset_none_detect directly, bypassing utils/datasets/__init__.py (heavy deps).
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(REPO_ROOT / "studio" / "backend" / "utils" / "datasets"))

from dataset_none_detect import (
    find_none_chatml,
    print_report,
    scan_dataset,
)

LOG_DIR = REPO_ROOT / "tests" / "logs"
LOG_DIR.mkdir(parents = True, exist_ok = True)
LOG_PATH = LOG_DIR / "none_detect_results.log"

# Helpers


class Tee:
    """Write to both stdout and a file simultaneously."""

    def __init__(self, file):
        self.file = file
        self.stdout = sys.stdout

    def write(self, data):
        self.stdout.write(data)
        self.file.write(data)

    def flush(self):
        self.stdout.flush()
        self.file.flush()


def section(title: str):
    line = "=" * 70
    print(f"\n{line}")
    print(f"  {title}")
    print(f"{line}")


def run_scan(
    dataset,
    label: str,
    fmt: str = "auto",
) -> dict | None:
    print(f"\n--- Scanning: {label} (fmt={fmt}) ---")
    try:
        stats = scan_dataset(dataset, fmt = fmt)
        print_report(stats, stats["format"])
        return stats
    except Exception as exc:
        print(f"  [ERROR] {exc}")
        traceback.print_exc()
        return None


def assert_bad_rows(stats: dict, expected_min: int, label: str):
    bad = len(stats.get("bad_row_indices", []))
    status = "PASS" if bad >= expected_min else "FAIL"
    print(f"  [{status}] {label}: found {bad} bad rows (expected >= {expected_min})")
    return status == "PASS"


def assert_exact_recall(stats: dict, expected_bad: set, label: str):
    """Every injected bad row index must appear in bad_row_indices."""
    actual_bad = set(stats.get("bad_row_indices", []))
    missed = expected_bad - actual_bad
    all_caught = len(missed) == 0
    status = "PASS" if all_caught else "FAIL"
    caught_count = len(expected_bad) - len(missed)
    print(
        f"  [{status}] {label}: exact recall — "
        f"{caught_count}/{len(expected_bad)} injected bad rows caught",
        end = "",
    )
    if missed:
        print(f"  (missed rows: {sorted(missed)})")
    else:
        print()
    return all_caught


# 1. Synthetic datasets

# Minimal mock for hand-crafted rows pyarrow can't represent (e.g. messages=None / "not a list").


class _MockDataset:
    """Behaves like an HF Dataset for iteration, len(), and index access."""

    def __init__(self, rows: list, columns: list):
        self.column_names = columns
        self._rows = rows

    def __len__(self):
        return len(self._rows)

    def __iter__(self):
        return iter(self._rows)

    def __getitem__(self, idx):
        """Support dataset[i] and dataset[i][col] patterns used by _probe_conversation."""
        return self._rows[idx]


def test_p1_fix():
    """find_none_chatml records rows where messages is None or non-list."""
    section("P1 Fix Verification — non-list conversation column values")

    sys.path.insert(0, str(REPO_ROOT / "tests" / "utils"))
    from generate_dataset_with_none import make_chatml_p1_rows

    p1_rows = make_chatml_p1_rows()
    mock_ds = _MockDataset(p1_rows, ["messages"])

    print(f"  Rows under test: {p1_rows}")
    stats = find_none_chatml(mock_ds, col = "messages")
    print_report(stats, "chatml")

    expected_bad = set(range(len(p1_rows)))
    actual_bad = set(stats.get("bad_row_indices", []))
    all_caught = expected_bad.issubset(actual_bad)
    print(
        f"  [{'PASS' if all_caught else 'FAIL'}] P1 fix: all {len(expected_bad)} non-list rows caught"
    )

    for row in stats.get("findings", []):
        vtype = row.get("value_type", "?")
        raw = row.get("raw_value", "?")
        print(f"    row {row['row_index']}: value_type={vtype!r}  raw={raw}")

    return stats


def test_probe_p1_fix():
    """scan_dataset(fmt='auto') on an all-corrupt messages column returns findings, not ValueError."""
    section("P1 Fix Verification — probe skip on all-corrupt column (auto-detect path)")

    # All rows have messages=None, so the probe finds no dict turn.
    all_corrupt_rows = [{"messages": None}] * 5
    mock_ds = _MockDataset(all_corrupt_rows, ["messages"])

    print(f"  Rows under test: {len(all_corrupt_rows)} rows all with messages=None")
    try:
        stats = scan_dataset(mock_ds, fmt = "auto")
        print_report(stats, stats.get("format", "?"))
        bad = len(stats.get("bad_row_indices", []))
        status = "PASS" if bad == len(all_corrupt_rows) else "FAIL"
        print(
            f"  [{status}] Probe P1 fix: {bad}/{len(all_corrupt_rows)} all-corrupt rows caught via auto-detect"
        )
        return stats
    except ValueError as exc:
        print(f"  [FAIL] scan_dataset raised ValueError (probe P1 bug NOT fixed): {exc}")
        return None


def test_probe_string_corrupt():
    """P2 fix: a plain-string 'messages' column must NOT be classified as chatml (raises ValueError)."""
    section("P2 Fix Verification — plain-string messages not classified as chatml")

    string_rows = [{"messages": "this is a string, not a list"}] * 5
    mock_ds = _MockDataset(string_rows, ["messages"])

    print(f"  Rows under test: {len(string_rows)} rows all with messages='string'")
    try:
        stats = scan_dataset(mock_ds, fmt = "auto")
        fmt = stats.get("format", "?")
        not_chatml = fmt != "chatml"
        status = "PASS" if not_chatml else "FAIL"
        print(
            f"  [{status}] String-corrupt probe: detected fmt={fmt!r} "
            f"(expected: anything except 'chatml')"
        )
        return stats
    except ValueError as exc:
        # ValueError (unknown format) is the correct outcome for a non-conversation column.
        print(
            f"  [PASS] String-corrupt probe: scan_dataset raised ValueError (not chatml, as expected): {exc}"
        )
        return None


def test_explicit_fmt_corrupt():
    """scan_dataset(fmt='chatml') on an all-corrupt column returns findings, not ValueError."""
    section("P1 Fix Verification — explicit fmt='chatml' on all-corrupt column")

    all_corrupt_rows = [{"messages": None}] * 4 + [{"messages": "not a list"}] * 3
    mock_ds = _MockDataset(all_corrupt_rows, ["messages"])

    print(f"  Rows under test: {len(all_corrupt_rows)} rows (4×None, 3×string)")
    try:
        stats = scan_dataset(mock_ds, fmt = "chatml")
        print_report(stats, stats.get("format", "?"))
        bad = len(stats.get("bad_row_indices", []))
        status = "PASS" if bad == len(all_corrupt_rows) else "FAIL"
        print(
            f"  [{status}] Explicit-fmt P1 fix: {bad}/{len(all_corrupt_rows)} rows caught with fmt='chatml'"
        )
        return stats
    except ValueError as exc:
        print(f"  [FAIL] scan_dataset raised ValueError (explicit-fmt P1 NOT fixed): {exc}")
        return None


def test_p2_probe_skips_corrupt_prefers_valid():
    """P2 fix: probe continues past a corrupt 'messages' column to a valid 'conversations' column."""
    section("P2 Fix Verification — probe continues past corrupt first column to valid second")

    # messages column is all-None; conversations is a valid ShareGPT column.
    rows = [
        {
            "messages": None,
            "conversations": [
                {"from": "human", "value": "Hello"},
                {"from": "gpt", "value": "Hi!"},
            ],
        }
    ] * 5 + [
        {
            "messages": None,
            "conversations": [
                {"from": "human", "value": ""},
                {"from": "gpt", "value": "OK"},
            ],
        }
    ] * 2
    mock_ds = _MockDataset(rows, ["messages", "conversations"])

    print(f"  Rows: {len(rows)} — messages=None, conversations=valid ShareGPT (2 bad value='')")
    try:
        stats = scan_dataset(mock_ds, fmt = "auto")
        fmt = stats.get("format", "?")
        col = stats.get("column", "?")
        bad = len(stats.get("bad_row_indices", []))
        # Must detect 'conversations' (sharegpt), not 'messages'.
        correct_col = col == "conversations"
        correct_fmt = fmt == "sharegpt"
        correct_bad = bad == 2
        status = "PASS" if (correct_col and correct_fmt and correct_bad) else "FAIL"
        print(
            f"  [{status}] Probe P2 fix: fmt={fmt!r} col={col!r} bad_rows={bad} "
            f"(expected fmt='sharegpt' col='conversations' bad=2)"
        )
        print_report(stats, fmt)
        return stats
    except ValueError as exc:
        print(f"  [FAIL] scan_dataset raised ValueError: {exc}")
        return None


def test_p2_explicit_fmt_col_priority():
    """P2 fix: explicit fmt='sharegpt' lets find_none_sharegpt pick its own column (conversations)."""
    section("P2 Fix Verification — explicit fmt='sharegpt' respects per-scanner column priority")

    # messages has valid role/content turns (chatml-ish); conversations has bad sharegpt turns.
    rows = [
        {
            "messages": [
                {"role": "user", "content": "Hi"},
                {"role": "assistant", "content": "Hello"},
            ],
            "conversations": [
                {"from": "human", "value": None},
                {"from": "gpt", "value": "OK"},
            ],
        }
    ] * 5
    mock_ds = _MockDataset(rows, ["messages", "conversations"])

    print(f"  Rows: {len(rows)} — messages=clean chatml, conversations=bad sharegpt (value=None)")
    stats = scan_dataset(mock_ds, fmt = "sharegpt")
    col = stats.get("column", "?")
    bad = len(stats.get("bad_row_indices", []))
    # fmt='sharegpt' scans 'conversations' -> 5 bad rows.
    correct_col = col == "conversations"
    correct_bad = bad == 5
    status = "PASS" if (correct_col and correct_bad) else "FAIL"
    print(
        f"  [{status}] Explicit-fmt P2 fix: col={col!r} bad_rows={bad} "
        f"(expected col='conversations' bad=5)"
    )
    print_report(stats, "sharegpt")
    return stats


def test_p2_gptoss_col_priority():
    """P2 fix: fmt='gptoss' scans 'messages' only, not a clean 'conversations' fallback."""
    section("P2 Fix Verification — fmt='gptoss' scans messages only, not conversations")

    # messages is all-None (corrupt); conversations is clean sharegpt.
    rows = [
        {
            "messages": None,
            "conversations": [
                {"from": "human", "value": "Hello"},
                {"from": "gpt", "value": "Hi!"},
            ],
        }
    ] * 5
    mock_ds = _MockDataset(rows, ["messages", "conversations"])

    print(f"  Rows: {len(rows)} — messages=None (corrupt), conversations=clean sharegpt")
    try:
        stats = scan_dataset(mock_ds, fmt = "gptoss")
        col = stats.get("column", "?")
        bad = len(stats.get("bad_row_indices", []))
        correct_col = col == "messages"
        correct_bad = bad == 5
        status = "PASS" if (correct_col and correct_bad) else "FAIL"
        print(
            f"  [{status}] gptoss P2 fix: col={col!r} bad_rows={bad} "
            f"(expected col='messages' bad=5)"
        )
        print_report(stats, "gptoss")
        return stats
    except ValueError as exc:
        print(f"  [FAIL] scan_dataset raised ValueError: {exc}")
        return None


def test_new_p1_explicit_sharegpt_both_all_corrupt():
    """NEW P1 (commit eb7fea3b7e): fmt='sharegpt' with both columns all-corrupt scans 'conversations', not 'messages'."""
    section(
        "NEW P1 — explicit fmt='sharegpt' scans 'conversations' even when both columns all-corrupt"
    )

    # Both columns are all-corrupt: every row has None.
    rows = [{"messages": None, "conversations": None}] * 5
    mock_ds = _MockDataset(rows, ["messages", "conversations"])

    print(f"  Rows: {len(rows)} — messages=None, conversations=None (both all-corrupt)")
    try:
        stats = scan_dataset(mock_ds, fmt = "sharegpt")
        col = stats.get("column", "?")
        bad = len(stats.get("bad_row_indices", []))
        # Must scan 'conversations', not 'messages'.
        correct_col = col == "conversations"
        correct_bad = bad == 5
        status = "PASS" if (correct_col and correct_bad) else "FAIL"
        print(
            f"  [{status}] New-P1 explicit sharegpt: col={col!r} bad_rows={bad} "
            f"(expected col='conversations' bad=5)"
        )
        print_report(stats, "sharegpt")
        return stats
    except ValueError as exc:
        print(f"  [FAIL] scan_dataset raised ValueError: {exc}")
        return None


def test_new_p2_plain_string_messages_not_chatml():
    """NEW P2 (commit eb7fea3b7e): plain-string 'messages' must NOT be auto-classified as chatml."""
    section("NEW P2 — plain-string 'messages' column must NOT be classified as chatml")

    # messages is a plain text column, not a conversation column.
    rows = [{"messages": "hello world"}] * 5
    mock_ds = _MockDataset(rows, ["messages"])

    print(f"  Rows: {len(rows)} — messages='hello world' (plain strings, not conversation)")
    try:
        stats = scan_dataset(mock_ds, fmt = "auto")
        fmt = stats.get("format", "?")
        not_chatml = fmt != "chatml"
        status = "PASS" if not_chatml else "FAIL"
        print(
            f"  [{status}] New-P2 plain-string messages: detected fmt={fmt!r} "
            f"(expected: anything except 'chatml')"
        )
        return stats
    except ValueError as exc:
        # ValueError is also acceptable: not a valid conversation format.
        print(
            f"  [PASS] New-P2 plain-string messages: scan_dataset raised ValueError (not chatml): {exc}"
        )
        return {
            "format": "unknown",
            "total_rows": 5,
            "bad_row_indices": [],
            "findings": [],
        }


def test_synthetic():
    section("1. Synthetic Datasets (generated in-memory)")

    sys.path.insert(0, str(REPO_ROOT / "tests" / "utils"))
    from generate_dataset_with_none import (
        make_alpaca_dataset,
        make_chatml_dataset,
        make_sharegpt_dataset,
    )

    results = {}

    # ChatML — 10 clean rows (0-9), 8 bad rows (10-17)
    ds_chatml = make_chatml_dataset()
    stats = run_scan(ds_chatml, "Synthetic ChatML (messages/role/content)")
    assert_bad_rows(stats, 8, "ChatML bad rows")
    assert_exact_recall(stats, set(range(10, 18)), "ChatML exact recall")
    results["chatml"] = stats

    # ShareGPT — 5 clean rows (0-4), 5 bad rows (5-9)
    ds_sgpt = make_sharegpt_dataset()
    stats = run_scan(ds_sgpt, "Synthetic ShareGPT (conversations/from/value)")
    assert_bad_rows(stats, 3, "ShareGPT bad rows")
    assert_exact_recall(stats, set(range(5, 10)), "ShareGPT exact recall")
    results["sharegpt"] = stats

    # Alpaca — 5 clean rows (0-4), 5 bad rows (5-9)
    ds_alpaca = make_alpaca_dataset()
    stats = run_scan(ds_alpaca, "Synthetic Alpaca (instruction/output)")
    assert_bad_rows(stats, 4, "Alpaca bad rows")
    assert_exact_recall(stats, set(range(5, 10)), "Alpaca exact recall")
    results["alpaca"] = stats

    return results


# 2. HuggingFace: peteromallet/dataclaw-peteromallet


def _brute_force_bad_rows(ds, fmt: str) -> set:
    """Pure-Python ground-truth scanner (no shared code with dataset_none_detect) for independent proof.

    Flags a row bad if any field/turn is None, empty, or whitespace-only; returns bad row indices.
    """

    def _blank(val) -> bool:
        if val is None:
            return True
        if isinstance(val, str) and val.strip() == "":
            return True
        return False

    bad: set = set()
    for i, row in enumerate(ds):
        if fmt in ("chatml", "gptoss"):
            msgs = row.get("messages")
            if msgs is None or not isinstance(msgs, list):
                bad.add(i)
                continue
            for turn in msgs:
                if turn is None or (isinstance(turn, dict) and _blank(turn.get("content"))):
                    bad.add(i)
                    break
        elif fmt == "sharegpt":
            convs = row.get("conversations")
            if convs is None or not isinstance(convs, list):
                bad.add(i)
                continue
            for turn in convs:
                if turn is None or (isinstance(turn, dict) and _blank(turn.get("value"))):
                    bad.add(i)
                    break
        elif fmt == "alpaca":
            if _blank(row.get("instruction")) or _blank(row.get("output")):
                bad.add(i)
    return bad


def _assert_hf_no_misses(ds, stats: dict, label: str) -> bool:
    """Independent check: scan_dataset() must find every bad row brute-force finds (no misses)."""
    fmt = stats.get("format", "unknown")
    module_bad = set(stats.get("bad_row_indices", []))

    print(f"  Running brute-force independent scan (fmt={fmt!r}, {len(ds)} rows)...")
    brute_bad = _brute_force_bad_rows(ds, fmt)

    missed = brute_bad - module_bad  # brute-force found, module missed
    extra = module_bad - brute_bad  # module flagged, brute-force didn't

    no_misses = len(missed) == 0
    snippet = ""
    if missed:
        sample = sorted(missed)[:10]
        snippet = f"  (first missed rows: {sample}{'...' if len(missed) > 10 else ''})"
    status = "PASS" if no_misses else "FAIL"
    print(
        f"  [{status}] {label} no-miss check — "
        f"brute-force: {len(brute_bad)} bad rows  |  "
        f"module: {len(module_bad)} bad rows  |  "
        f"missed: {len(missed)}{snippet}"
    )
    if extra:
        # Module may legitimately flag more rows (extra structural checks); informational only.
        print(
            f"  [INFO] {label} — module flagged {len(extra)} rows not in brute-force "
            f"(may reflect additional structural checks, not false positives)"
        )
    return no_misses


def test_dataclaw():
    section("2. HuggingFace — peteromallet/dataclaw-peteromallet")
    try:
        from datasets import load_dataset

        print("  Loading dataset (streaming first 500 rows for speed)...")
        ds = load_dataset(
            "peteromallet/dataclaw-peteromallet",
            split = "train",
            streaming = False,
        )
        print(f"  Loaded {len(ds)} rows, columns: {ds.column_names}")
        stats = run_scan(ds, "dataclaw-peteromallet")
        if stats:
            _assert_hf_no_misses(ds, stats, "dataclaw-peteromallet")
        return stats
    except Exception as exc:
        print(f"  [ERROR] Could not load dataclaw dataset: {exc}")
        traceback.print_exc()
        return None


# 3. HuggingFace: peteromallet/my-personal-codex-data


def test_codex_data():
    section("3. HuggingFace — peteromallet/my-personal-codex-data")
    try:
        # load_dataset fails here (ujson chokes on the large JSONL batch); download + parse raw instead.
        from huggingface_hub import hf_hub_download
        from datasets import Dataset

        print("  Downloading conversations.jsonl via huggingface_hub...")
        path = hf_hub_download(
            "peteromallet/my-personal-codex-data",
            "conversations.jsonl",
            repo_type = "dataset",
        )
        rows = []
        with open(path, encoding = "utf-8") as f:
            for line in f:
                line = line.strip()
                if line:
                    rows.append(json.loads(line))
        ds = Dataset.from_list(rows)
        print(f"  Loaded {len(ds)} rows, columns: {ds.column_names}")
        stats = run_scan(ds, "my-personal-codex-data")
        if stats:
            _assert_hf_no_misses(ds, stats, "my-personal-codex-data")
        return stats
    except Exception as exc:
        print(f"  [ERROR] Could not load codex dataset: {exc}")
        traceback.print_exc()
        return None


# Main


def main():
    started = datetime.now().isoformat()

    with open(LOG_PATH, "w", encoding = "utf-8") as log_file:
        sys.stdout = Tee(log_file)

        print(f"dataset_none_detect.py — Test Run")
        print(f"Started: {started}")
        print(f"Python:  {sys.version}")
        print(f"Log:     {LOG_PATH}")

        all_results = {}

        all_results["p1_fix"] = test_p1_fix()
        all_results["probe_p1_fix"] = test_probe_p1_fix()
        all_results["probe_string_corrupt"] = test_probe_string_corrupt()
        all_results["explicit_fmt_corrupt"] = test_explicit_fmt_corrupt()
        all_results["p2_probe_valid_fallback"] = test_p2_probe_skips_corrupt_prefers_valid()
        all_results["p2_explicit_col_priority"] = test_p2_explicit_fmt_col_priority()
        all_results["p2_gptoss_col_priority"] = test_p2_gptoss_col_priority()
        all_results["new_p1_sharegpt_all_corrupt"] = (
            test_new_p1_explicit_sharegpt_both_all_corrupt()
        )
        all_results["new_p2_plain_string_not_chatml"] = (
            test_new_p2_plain_string_messages_not_chatml()
        )
        all_results["synthetic"] = test_synthetic()
        all_results["dataclaw"] = test_dataclaw()
        all_results["codex_data"] = test_codex_data()

        # Summary table
        section("SUMMARY")
        rows = [
            ("Dataset", "Format", "Total rows", "Bad rows", "Bad turns"),
        ]

        def _row(label, stats):
            if stats is None:
                return (label, "ERROR", "-", "-", "-")
            fmt = stats.get("format", "?")
            total = stats.get("total_rows", "?")
            bad = len(stats.get("bad_row_indices", []))
            turns = stats.get("total_none_turns") or len(stats.get("findings", []))
            return (label, fmt, str(total), str(bad), str(turns))

        for key, label in [
            ("chatml", "Synthetic chatml"),
            ("sharegpt", "Synthetic sharegpt"),
            ("alpaca", "Synthetic alpaca"),
        ]:
            s = all_results.get("synthetic") or {}
            rows.append(_row(label, s.get(key) if isinstance(s, dict) else None))

        rows.append(_row("dataclaw-peteromallet", all_results.get("dataclaw")))
        rows.append(_row("my-personal-codex-data", all_results.get("codex_data")))

        col_widths = [max(len(r[i]) for r in rows) for i in range(5)]
        fmt_str = "  " + "  ".join(f"{{:<{w}}}" for w in col_widths)
        header = rows[0]
        print(fmt_str.format(*header))
        print("  " + "-" * (sum(col_widths) + 10))
        for row in rows[1:]:
            print(fmt_str.format(*row))

        # Write machine-readable JSON summary alongside the log
        json_path = LOG_DIR / "none_detect_results.json"
        summary = {}
        for key, val in all_results.items():
            if val is None:
                summary[key] = None
            elif isinstance(val, dict):
                # Flatten synthetic sub-keys
                if key == "synthetic":
                    for subkey, subval in val.items():
                        summary[f"synthetic_{subkey}"] = (
                            {
                                "format": subval.get("format"),
                                "total_rows": subval.get("total_rows"),
                                "bad_row_count": len(subval.get("bad_row_indices", [])),
                                "bad_turn_count": subval.get(
                                    "total_none_turns", len(subval.get("findings", []))
                                ),
                            }
                            if subval
                            else None
                        )
                else:
                    summary[key] = {
                        "format": val.get("format"),
                        "total_rows": val.get("total_rows"),
                        "bad_row_count": len(val.get("bad_row_indices", [])),
                        "bad_turn_count": val.get("total_none_turns", len(val.get("findings", []))),
                    }

        json_path.write_text(json.dumps(summary, indent = 2), encoding = "utf-8")

        finished = datetime.now().isoformat()
        print(f"\nFinished: {finished}")
        print(f"Log:      {LOG_PATH}")
        print(f"JSON:     {json_path}")

        sys.stdout = sys.stdout.stdout  # restore


if __name__ == "__main__":
    main()
