"""Tests for atm.validate — offline calibration validation.

Covers the 3 tests from plan section D':
  17. test_validate_calibration_pass
  18. test_validate_calibration_fail_reports_top_candidates
  19. test_validate_calibration_file_not_found
"""
from __future__ import annotations

import json
from pathlib import Path

import numpy as np
import pytest

from atm.config import (
    CanaryRegion,
    ColorSpec,
    Config,
    DiscordCfg,
    ROI,
    TelegramCfg,
    YAxisCalib,
)
from atm.detector import DetectionResult
from atm.vision import ColorMatch


def _make_config() -> Config:
    """Minimal Config with a palette large enough to support top-3 candidates."""
    colors = {
        "turquoise":   ColorSpec(rgb=(0, 200, 200),    tolerance=30),
        "yellow":      ColorSpec(rgb=(255, 255, 0),    tolerance=30),
        "dark_green":  ColorSpec(rgb=(0, 100, 0),      tolerance=30),
        "dark_red":    ColorSpec(rgb=(165, 42, 42),    tolerance=30),
        "light_green": ColorSpec(rgb=(144, 238, 144),  tolerance=30),
        "light_red":   ColorSpec(rgb=(255, 182, 193),  tolerance=30),
        "gray":        ColorSpec(rgb=(128, 128, 128),  tolerance=30),
        "background":  ColorSpec(rgb=(18, 18, 18),     tolerance=15),
    }
    return Config(
        window_title="test",
        dot_roi=ROI(x=0, y=0, w=100, h=100),
        chart_roi=ROI(x=0, y=0, w=100, h=100),
        colors=colors,
        y_axis=YAxisCalib(p1_y=0, p1_price=100.0, p2_y=100, p2_price=0.0),
        canary=CanaryRegion(
            roi=ROI(x=0, y=0, w=10, h=10),
            baseline_phash="0" * 64,
        ),
        discord=DiscordCfg(webhook_url="http://localhost/fake"),
        telegram=TelegramCfg(bot_token="fake_token", chat_id="123"),
        debounce_depth=1,
    )


def _write_labels(tmp_path: Path, entries: list[dict]) -> Path:
    f = tmp_path / "labels.json"
    f.write_text(json.dumps(entries), encoding="utf-8")
    return f


def _write_blank_png(tmp_path: Path, name: str) -> Path:
    """Write a trivially-valid 10x10 BGR image so cv2.imread returns non-None."""
    import cv2
    p = tmp_path / name
    arr = np.zeros((10, 10, 3), dtype=np.uint8)
    cv2.imwrite(str(p), arr)
    return p


# ---------------------------------------------------------------------------
# Test 17: PASS path — mocked Detector.step returns expected color
# ---------------------------------------------------------------------------

def test_validate_calibration_pass(monkeypatch, tmp_path):
    from atm import validate as validate_mod

    img_path = _write_blank_png(tmp_path, "yellow_sample.png")
    labels = _write_labels(
        tmp_path,
        [{"path": str(img_path), "expected": "yellow", "note": "test"}],
    )

    def fake_step(self, ts, frame=None):
        return DetectionResult(
            ts=ts,
            window_found=True,
            dot_found=True,
            rgb=(250, 250, 5),
            match=ColorMatch(name="yellow", distance=6.0, confidence=0.94),
            accepted=True,
            color="yellow",
        )

    monkeypatch.setattr("atm.detector.Detector.step", fake_step)

    report = validate_mod.validate_calibration(labels, _make_config())

    assert report.total == 1
    assert report.passed == 1
    assert report.failed == 0
    assert report.all_pass is True
    rec = report.records[0]
    assert rec.passed is True
    assert rec.detected == "yellow"
    assert rec.expected == "yellow"
    assert "[PASS]" in report.render()

    # CLI wiring: exit 0
    import atm.main as _main

    class _Args:
        label_file = labels

    monkeypatch.setattr("atm.config.Config.load_current", classmethod(lambda cls, d: _make_config()))
    with pytest.raises(SystemExit) as exc_info:
        _main._cmd_validate_calibration(_Args())
    assert exc_info.value.code == 0


# ---------------------------------------------------------------------------
# Test 18: FAIL path — Detector returns wrong color; report lists top 3
#                      candidates and a SUGGESTIONS line with RGB distance.
# ---------------------------------------------------------------------------

def test_validate_calibration_fail_reports_top_candidates(monkeypatch, tmp_path):
    from atm import validate as validate_mod

    img_path = _write_blank_png(tmp_path, "dark_red_sample.png")
    labels = _write_labels(
        tmp_path,
        [{"path": str(img_path), "expected": "dark_red", "note": "missed dark_red"}],
    )

    # Observed RGB closer to gray than dark_red (like the real 2026-04-17 miss).
    def fake_step(self, ts, frame=None):
        return DetectionResult(
            ts=ts,
            window_found=True,
            dot_found=True,
            rgb=(135, 62, 67),
            match=ColorMatch(name="gray", distance=45.0, confidence=0.12),
            accepted=True,
            color="gray",
        )

    monkeypatch.setattr("atm.detector.Detector.step", fake_step)

    report = validate_mod.validate_calibration(labels, _make_config())

    assert report.total == 1
    assert report.failed == 1
    assert report.all_pass is False

    rec = report.records[0]
    assert rec.passed is False
    assert rec.detected == "gray"
    assert rec.expected == "dark_red"
    # Top 3 candidates populated (name, score) sorted by RGB distance.
    assert len(rec.top3) == 3
    names = [n for n, _ in rec.top3]
    # dark_red should appear in top candidates since observed RGB(135,62,67)
    # is reasonably close to dark_red(165,42,42).
    assert "dark_red" in names

    rendered = report.render()
    assert "[FAIL]" in rendered
    assert "Top 3 candidates:" in rendered
    assert "SUGGESTIONS:" in rendered
    # The suggestion must mention the expected color's RGB and the measured distance.
    assert "dark_red" in rendered
    assert "(165, 42, 42)" in rendered

    # CLI wiring: exit 1
    import atm.main as _main

    class _Args:
        label_file = labels

    monkeypatch.setattr("atm.config.Config.load_current", classmethod(lambda cls, d: _make_config()))
    with pytest.raises(SystemExit) as exc_info:
        _main._cmd_validate_calibration(_Args())
    assert exc_info.value.code == 1


# ---------------------------------------------------------------------------
# Test 19: missing label file — clean error, non-zero exit, no stack trace
# ---------------------------------------------------------------------------

def test_validate_calibration_file_not_found(monkeypatch, tmp_path, capsys):
    from atm import validate as validate_mod

    missing = tmp_path / "nope.json"

    # Library-level: raises ValidationError (not bare FileNotFoundError).
    with pytest.raises(validate_mod.ValidationError) as exc_info:
        validate_mod.validate_calibration(missing, _make_config())
    assert "not found" in str(exc_info.value).lower()

    # CLI-level: graceful sys.exit with non-zero code, message on stderr.
    import atm.main as _main

    class _Args:
        label_file = missing

    monkeypatch.setattr("atm.config.Config.load_current", classmethod(lambda cls, d: _make_config()))
    with pytest.raises(SystemExit) as exc_info:
        _main._cmd_validate_calibration(_Args())
    assert exc_info.value.code != 0
    err = capsys.readouterr().err
    assert "not found" in err.lower()
    # Ensure no python traceback leaked through.
    assert "Traceback" not in err
