"""
Unit tests for seq_interp.src.gui.adapters - pure data-conversion helpers,
no Qt or hardware dependencies required.
"""
import math
import pytest
import numpy as np

from seq_interp.src.gui.adapters import (
    gate_to_step,
    build_block_rows,
    find_block_at_time,
    validate_timing,
    seq_metadata,
    block_cumtimes,
)


# --- Fixtures / helpers --------------------------------------------------------

class _HW:
    """Minimal stand-in for HardwareConstraints."""
    rf_raster_time       = 1e-6
    grad_raster_time     = 10e-6
    adc_raster_time      = 100e-9
    block_duration_raster = 10e-6
    RF_DELAY             = 800e-9
    TR_DELAY             = 800e-9
    START_DELAY          = 1600e-9
    MIN_BLOCK_DURATION   = 20e-9
    gamma                = 42.576e6
    GRAD_MAX             = 9e-3 * 42.576e6


def _make_seq_data(blocks: list, rf=None, t_rf=None,
                   gx=None, t_gx=None, gy=None, t_gy=None, gz=None, t_gz=None):
    return {
        "blocks": blocks,
        "params": {"scale_rf": 1.0},
        "rf":  rf   if rf   is not None else np.array([]),
        "t_rf": t_rf if t_rf is not None else np.array([]),
        "gx":  gx   if gx   is not None else np.array([]),
        "t_gx": t_gx if t_gx is not None else np.array([]),
        "gy":  gy   if gy   is not None else np.array([]),
        "t_gy": t_gy if t_gy is not None else np.array([]),
        "gz":  gz   if gz   is not None else np.array([]),
        "t_gz": t_gz if t_gz is not None else np.array([]),
    }


def _make_sync_data(gate_rf, gate_adc, gate_tr, durs, n_blocks=None):
    return {
        "gate_rf":         list(gate_rf),
        "gate_adc":        list(gate_adc),
        "gate_tr_switch":  list(gate_tr),
        "blocks_duration": list(durs),
        "number_of_blocks": n_blocks if n_blocks is not None else len(durs) - 1,
        "synchro_block_timer": 20e-9,
        "min_block_time":  800e-9,
    }


# --- gate_to_step --------------------------------------------------------------

class TestGateToStep:
    def test_output_length(self):
        gate = [0, 1, 0]
        durs = [1e-3, 2e-3, 1e-3]
        t, v = gate_to_step(gate, durs)
        assert len(t) == 6
        assert len(v) == 6

    def test_values_match_gate(self):
        gate = [0, 1, 0]
        durs = [1e-3, 2e-3, 1e-3]
        t, v = gate_to_step(gate, durs)
        assert v[0] == 0
        assert v[2] == 1
        assert v[4] == 0

    def test_time_monotonic(self):
        gate = [1, 0, 1, 0]
        durs = [0.5e-3] * 4
        t, _ = gate_to_step(gate, durs)
        assert all(t[i] <= t[i + 1] for i in range(len(t) - 1))

    def test_empty_gate(self):
        t, v = gate_to_step([], [])
        assert len(t) == 0 and len(v) == 0

    def test_step_coverage(self):
        """Each gate value spans exactly its block duration."""
        gate = [1]
        durs = [2e-3]
        t, v = gate_to_step(gate, durs)
        assert math.isclose(t[1] - t[0], 2e-3)


# --- block_cumtimes ------------------------------------------------------------

class TestBlockCumtimes:
    def test_starts_at_zero(self):
        ct = block_cumtimes([1e-3, 2e-3, 3e-3])
        assert ct[0] == pytest.approx(0.0)

    def test_length(self):
        ct = block_cumtimes([1e-3, 2e-3])
        assert len(ct) == 3

    def test_values(self):
        ct = block_cumtimes([1e-3, 2e-3, 3e-3])
        assert ct[-1] == pytest.approx(6e-3)


# --- build_block_rows ---------------------------------------------------------

class TestBuildBlockRows:
    def _simple_sync(self, blocks):
        """
        Produce a minimal consistent sync_data that matches what Synchronizer would
        generate for the given block list.  Keeps bookkeeping manual but deterministic.
        """
        start = 1.6e-6
        rf_delay = 0.8e-6
        tr_delay = 0.8e-6
        block_dur = 5e-3

        g_rf, g_adc, g_tr, durs = [0], [0], [1], [start]
        for blk in blocks:
            has_adc = blk.get("has_adc", False)
            has_rf  = "RF" in blk.get("type", [])
            if has_adc:
                g_adc.append(0); g_rf.append(g_rf[-1])
                durs[-1] -= tr_delay; durs.append(tr_delay)
                g_tr.append(0)
                g_adc.append(1); g_tr.append(0)
            else:
                g_tr.append(1); g_adc.append(0)
            if has_rf and not has_adc:
                g_rf.append(1); g_adc.append(g_adc[-1])
                durs[-1] -= rf_delay; durs.append(rf_delay)
                g_tr.append(g_tr[-1])
                g_rf.append(1)
            else:
                g_rf.append(0)
            durs.append(block_dur)
        return _make_sync_data(g_rf, g_adc, g_tr, durs)

    def test_start_delay_is_first_row(self):
        blocks = [{"type": [], "duration": 5e-3, "has_adc": False}]
        sync = self._simple_sync(blocks)
        seq  = _make_seq_data(blocks)
        rows = build_block_rows(seq, sync)
        assert rows[0].is_delay
        assert rows[0].delay_type == "START"
        assert rows[0].sync_index == 0

    def test_plain_block_no_delay_inserted(self):
        blocks = [{"type": [], "duration": 5e-3, "has_adc": False}]
        sync = self._simple_sync(blocks)
        seq  = _make_seq_data(blocks)
        rows = build_block_rows(seq, sync)
        # start_delay + 1 actual block = 2 rows
        assert len(rows) == 2
        assert not rows[1].is_delay

    def test_rf_block_inserts_rf_delay(self):
        blocks = [{"type": ["RF"], "duration": 5e-3, "has_adc": False}]
        sync = self._simple_sync(blocks)
        seq  = _make_seq_data(blocks)
        rows = build_block_rows(seq, sync)
        # start_delay + RF_DELAY + RF_block = 3 rows
        assert len(rows) == 3
        assert rows[1].delay_type == "RF"
        assert rows[2].has_rf

    def test_adc_block_inserts_tr_delay(self):
        blocks = [{"type": ["ADC"], "duration": 5e-3, "has_adc": True}]
        sync = self._simple_sync(blocks)
        seq  = _make_seq_data(blocks)
        rows = build_block_rows(seq, sync)
        # start_delay + TR_DELAY + ADC_block = 3 rows
        assert len(rows) == 3
        assert rows[1].delay_type == "TR"
        assert rows[2].has_adc

    def test_t_start_t_end_coverage(self):
        blocks = [
            {"type": [], "duration": 5e-3, "has_adc": False},
            {"type": [], "duration": 5e-3, "has_adc": False},
        ]
        sync = self._simple_sync(blocks)
        seq  = _make_seq_data(blocks)
        rows = build_block_rows(seq, sync)
        for r in rows:
            assert r.t_end > r.t_start

    def test_empty_blocks(self):
        sync = _make_sync_data([0], [0], [1], [1.6e-6])
        seq  = _make_seq_data([])
        rows = build_block_rows(seq, sync)
        assert len(rows) == 1
        assert rows[0].delay_type == "START"


# --- find_block_at_time -------------------------------------------------------

class TestFindBlockAtTime:
    def _rows(self):
        blocks = [{"type": [], "duration": 5e-3, "has_adc": False}] * 3
        sync   = {
            "gate_rf": [0, 0, 0, 0], "gate_adc": [0, 0, 0, 0],
            "gate_tr_switch": [1, 1, 1, 1],
            "blocks_duration": [1e-3, 5e-3, 5e-3, 5e-3],
            "number_of_blocks": 3, "synchro_block_timer": 20e-9,
            "min_block_time": 800e-9,
        }
        return build_block_rows(_make_seq_data(blocks), sync)

    def test_finds_correct_row(self):
        rows = self._rows()
        # First block: [0, 1e-3), second: [1e-3, 6e-3) ...
        found = find_block_at_time(0.5e-3, rows)
        assert found is not None
        assert found.t_start == pytest.approx(0.0)

    def test_returns_none_past_end(self):
        rows = self._rows()
        found = find_block_at_time(1e3, rows)
        assert found is None

    def test_boundary_belongs_to_next(self):
        rows = self._rows()
        # Exactly at t=1e-3 should be the SECOND block (t_start=1e-3)
        found = find_block_at_time(1e-3, rows)
        assert found is not None
        assert found.t_start == pytest.approx(1e-3)


# --- validate_timing ----------------------------------------------------------

class TestValidateTiming:
    def test_no_warnings_for_valid_data(self):
        hw = _HW()
        seq = _make_seq_data([])
        sync = _make_sync_data([0], [0], [1], [20e-9])
        warns = validate_timing(hw, seq, sync)
        assert warns == []

    def test_rf_delay_below_min(self):
        hw = _HW()
        hw.RF_DELAY = 1e-9   # below MIN_BLOCK_DURATION = 20 ns
        seq = _make_seq_data([])
        sync = _make_sync_data([0], [0], [1], [20e-9])
        warns = validate_timing(hw, seq, sync)
        assert any("RF_DELAY" in w for w in warns)

    def test_cl_rounds_to_zero(self):
        hw = _HW()
        # synchro_block_timer = 20e-9; duration = 10e-9 -> CL = 0
        seq = _make_seq_data([])
        sync = _make_sync_data([0], [0], [1], [10e-9])
        sync["synchro_block_timer"] = 20e-9
        warns = validate_timing(hw, seq, sync)
        assert any("CL rounds to zero" in w for w in warns)

    def test_waveform_missing(self):
        hw = _HW()
        seq = {"blocks": [], "params": {}}  # no rf/t_rf
        sync = _make_sync_data([0], [0], [1], [20e-9])
        warns = validate_timing(hw, seq, sync)
        assert any("'rf'" in w or "'t_rf'" in w for w in warns)

    def test_length_mismatch(self):
        hw = _HW()
        seq = _make_seq_data([], rf=np.array([1.0, 2.0]), t_rf=np.array([0.0]))
        sync = _make_sync_data([0], [0], [1], [20e-9])
        warns = validate_timing(hw, seq, sync)
        assert any("mismatch" in w.lower() for w in warns)


# --- seq_metadata -------------------------------------------------------------

class TestSeqMetadata:
    def test_counts(self):
        hw = _HW()
        blocks = [
            {"type": ["RF"], "duration": 5e-3, "has_adc": False},
            {"type": ["ADC"], "duration": 5e-3, "has_adc": True},
            {"type": ["GRAD"], "duration": 5e-3, "has_adc": False},
            {"type": [], "duration": 5e-3, "has_adc": False},
        ]
        meta = seq_metadata(_make_seq_data(blocks), hw)
        assert meta["Total blocks (orig)"] == 4
        assert meta["RF blocks"] == 1
        assert meta["ADC blocks"] == 1
        assert meta["Grad blocks"] == 1

    def test_raster_times_in_human_units(self):
        hw = _HW()
        meta = seq_metadata(_make_seq_data([]), hw)
        assert float(meta["RF raster (uss)"])   == pytest.approx(1.0)
        assert float(meta["Grad raster (uss)"]) == pytest.approx(10.0)
        assert float(meta["ADC raster (ns)"])  == pytest.approx(100.0)
