#!/usr/bin/env python3
"""test_gap_fill.py — 実測角の抜けの埋め方 (build_correction_episodes --gap-fill. 依頼 46) の確かめ.

  python3 pi05/test_gap_fill.py -v

ファイルを読まない部分だけ (KIOXIA にもアームにも触らない).
"""
import sys
import unittest
from pathlib import Path

import numpy as np

_HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(_HERE))

import build_correction_episodes as bce  # noqa: E402


def samples():
    """0〜1 秒は 0.1 秒おきに止まって読めている → 1.0〜2.6 秒は送信で抜け (この間に j1 が 6° 動く)
    → 2.6〜3.6 秒はまた 0.1 秒おき."""
    a = np.round(np.arange(0.0, 1.01, 0.1), 3)
    b = np.round(np.arange(2.6, 3.61, 0.1), 3)
    at = np.concatenate([a, b])
    aq = np.zeros((len(at), 6))
    aq[len(a):, 1] = 6.0
    return at, aq


class TestFill(unittest.TestCase):
    def test_linear_is_interp(self):
        at, aq = samples()
        t = np.arange(0.0, 3.6, 0.1)
        q, gaps = bce.fill_motion_gaps(t, at, aq, mode="linear")
        self.assertTrue(np.allclose(q, np.stack([np.interp(t, at, aq[:, k]) for k in range(6)], axis=1)))
        self.assertEqual(len(gaps), 1)

    def test_speed20_moves_then_holds(self):
        at, aq = samples()
        t = np.arange(0.0, 3.6, 0.1)
        q, gaps = bce.fill_motion_gaps(t, at, aq, mode="speed20")
        self.assertAlmostEqual(gaps[0]["move_sec"], 6.0 / bce.SPEED20_DEG_PER_SEC)
        steps = np.abs(np.diff(q[:, 1]))
        self.assertLessEqual(steps.max(), bce.SPEED20_DEG_PER_SEC * 0.1 + 1e-6)   # 1 コマ 3° まで
        i = np.flatnonzero(t >= 1.25)[0]
        self.assertAlmostEqual(q[i, 1], 6.0)                                      # 0.2 秒で着いて止まる
        self.assertTrue(np.allclose(q[t <= 1.0], 0.0))                            # 抜けの前は実測のまま
        self.assertTrue(np.allclose(q[t >= 2.6, 1], 6.0))                            # 抜けのあとも実測のまま
        self.assertTrue(np.allclose(q[:, [0, 2, 3, 4, 5]], 0.0))                  # 動かない関節は動かさない

    def test_speed20_long_move_uses_whole_gap(self):
        at, aq = samples()
        aq[aq[:, 1] > 0, 1] = 90.0                     # 1.6 秒では速度 20 で着かない動き
        t = np.arange(0.0, 3.6, 0.1)
        q1, _ = bce.fill_motion_gaps(t, at, aq, mode="speed20")
        q0, _ = bce.fill_motion_gaps(t, at, aq, mode="linear")
        self.assertTrue(np.allclose(q1, q0))

    def test_unknown_mode(self):
        at, aq = samples()
        with self.assertRaises(ValueError):
            bce.fill_motion_gaps(np.arange(0, 1, 0.1), at, aq, mode="spline")


class TestMatch(unittest.TestCase):
    def test_match_correction_and_chunk(self):
        q0, q1, q2, q3 = (np.zeros(6), np.r_[0, 6, 0, 0, 0, 0.], np.r_[0, 12, 0, 0, 0, 0.],
                          np.r_[2, 12, 0, 0, 0, 0.])
        gaps = [{"ta": 1.0, "tb": 2.6, "move_deg": 6.0, "qb": q1},
                {"ta": 2.7, "tb": 4.3, "move_deg": 6.0, "qb": q2},
                {"ta": 10.0, "tb": 11.6, "move_deg": 2.0, "qb": q3},
                {"ta": 20.0, "tb": 23.6, "move_deg": 0.0, "qb": q3}]     # 爪 (動いていない)
        log = [{"chunk": 0, "t_exec_start": 0.9, "t_exec_end": 4.5,
                "sent_targets": [[0, 6.5, 0, 0, 0, 0], [0, 12, 0, 0, 0, 0]]},
               {"type": "correction", "action": "move", "sent": True, "t": 10.02, "after": [2, 12, 0, 0, 0, 0]},
               {"type": "correction", "action": "accept"}]
        m = bce.match_gaps_to_sends(gaps, log)
        self.assertEqual(m["motion_gaps"], 3)
        self.assertEqual(m["matched_chunk"], 2)
        self.assertEqual(m["matched_correction"], 1)
        self.assertEqual(m["unmatched"], 0)
        self.assertEqual(m["max_arrival_err_deg"], 0.5)

    def test_ambiguous_correction_not_paired(self):
        """codex 推奨: 補正の時刻が 2 つの抜けの候補に入るときは組にしない (差の集計から外す)."""
        gaps = [{"ta": 1.0, "tb": 2.6, "move_deg": 6.0, "qb": np.zeros(6)},
                {"ta": 2.7, "tb": 4.3, "move_deg": 6.0, "qb": np.ones(6) * 9}]
        log = [{"type": "correction", "action": "move", "sent": True, "t": 2.5, "after": [0] * 6}]
        m = bce.match_gaps_to_sends(gaps, log)
        self.assertEqual(m["matched_correction"], 0)
        self.assertEqual(m["ambiguous"], 1)
        self.assertIsNone(m["max_arrival_err_deg"])

    def test_chunk_count_mismatch(self):
        gaps = [{"ta": 1.0, "tb": 2.6, "move_deg": 6.0, "qb": np.zeros(6)}]
        log = [{"chunk": 0, "t_exec_start": 0.9, "t_exec_end": 4.5, "sent_targets": [[0] * 6, [0] * 6]}]
        m = bce.match_gaps_to_sends(gaps, log)
        self.assertEqual(m["chunk_count_mismatch"], 1)
        self.assertEqual(m["unmatched"], 1)


if __name__ == "__main__":
    unittest.main()
