"""
Tests for Synchronizer.process — the synchro-sequence builder.

Covers the constraints added for the LF-MRI hardware:
  * minimum event duration = 20 clock ticks (real duration, TR preserved);
  * maximum block duration = 999999 ticks (long blocks split into equal parts);
  * ADC stays a single continuous HIGH level when its block is split;
  * the TR/RF/START delay enable flags (no 1/0-tick artifacts);
  * parallel gate arrays stay length-balanced.
"""
from __future__ import annotations

import pytest

from seq_interp.src.core.synchronizer import Synchronizer
from seq_interp.src.hardware.constraints import HardwareConstraints

TIMER = 20e-9          # MIN_BLOCK_DURATION — one clock tick
MIN_TICKS = 20
MAX_TICKS = 999999


def _hw(**flags) -> HardwareConstraints:
    hw = HardwareConstraints()
    for k, v in flags.items():
        setattr(hw, k, v)
    return hw


def _ticks(sync_data) -> list[int]:
    t = sync_data["synchro_block_timer"]
    return [round(d / t) for d in sync_data["blocks_duration"]]


def _array_lengths(sync_data) -> set[int]:
    return {len(sync_data[k]) for k in
            ("gate_adc", "gate_rf", "gate_tr_switch", "blocks_duration")}


# --------------------------------------------------------------------------- #
# Array bookkeeping
# --------------------------------------------------------------------------- #

def test_gate_arrays_balanced_and_seed_offset(make_seq):
    """All gate arrays share one length == number_of_blocks + 1 (leading seed)."""
    seq = make_seq([{"rf": True, "dur": 1e-3},
                    {"dur": 5e-3},
                    {"adc": True, "dur": 2e-3}])
    d = Synchronizer(_hw()).process(seq)
    lengths = _array_lengths(d)
    assert len(lengths) == 1
    assert lengths.pop() == d["number_of_blocks"] + 1


# --------------------------------------------------------------------------- #
# Minimum event duration (20 ticks), TR preserved
# --------------------------------------------------------------------------- #

def test_min_event_is_20_ticks(make_seq):
    """No emitted block is shorter than 20 ticks, none is zero."""
    seq = make_seq([{"rf": True, "dur": 1e-3},
                    {"adc": True, "dur": 2e-3}])
    d = Synchronizer(_hw()).process(seq)
    assert min(_ticks(d)) >= MIN_TICKS


def test_total_duration_preserved_by_min_clamp(make_seq):
    """Stretching short blocks is compensated from a neighbour — TR unchanged."""
    seq = make_seq([{"rf": True, "dur": 1e-3},
                    {"dur": 5e-3},
                    {"adc": True, "dur": 2e-3}])
    hw = _hw()
    total_in = sum(seq.block_durations.values()) + max(hw.START_DELAY, hw.RF_DELAY)
    d = Synchronizer(hw).process(seq)
    assert sum(d["blocks_duration"]) == pytest.approx(total_in)


# --------------------------------------------------------------------------- #
# Maximum block duration (999999 ticks) — splitting
# --------------------------------------------------------------------------- #

def test_long_block_split_under_max(make_seq):
    """A 50 ms ADC block (2.5M ticks) is split so every part <= 999999 ticks."""
    seq = make_seq([{"adc": True, "dur": 50e-3}])
    d = Synchronizer(_hw()).process(seq)
    assert max(_ticks(d)) <= MAX_TICKS


def test_split_preserves_total_duration(make_seq):
    seq = make_seq([{"adc": True, "dur": 50e-3}])
    hw = _hw()
    total_in = 50e-3 + max(hw.START_DELAY, hw.RF_DELAY)
    d = Synchronizer(hw).process(seq)
    assert sum(d["blocks_duration"]) == pytest.approx(total_in)


def test_split_keeps_adc_high_continuous(make_seq):
    """Split parts of an ADC block all stay HIGH (level, not per-block pulse)."""
    seq = make_seq([{"adc": True, "dur": 50e-3}])
    d = Synchronizer(_hw()).process(seq)
    high = [i for i, v in enumerate(d["gate_adc"]) if v == 1]
    # contiguous run, more than one part
    assert len(high) >= 2
    assert high == list(range(high[0], high[0] + len(high)))


# --------------------------------------------------------------------------- #
# Delay enable flags — no inserted block when off
# --------------------------------------------------------------------------- #

def test_tr_delay_flag_removes_inserted_block(make_seq):
    seq = make_seq([{"adc": True, "dur": 2e-3}])
    on = Synchronizer(_hw(TR_DELAY_ENABLED=True)).process(seq)
    off = Synchronizer(_hw(TR_DELAY_ENABLED=False)).process(seq)
    assert off["number_of_blocks"] == on["number_of_blocks"] - 1
    assert min(_ticks(off)) >= MIN_TICKS


def test_rf_delay_flag_removes_inserted_block(make_seq):
    seq = make_seq([{"rf": True, "dur": 1e-3}])
    on = Synchronizer(_hw(RF_DELAY_ENABLED=True)).process(seq)
    off = Synchronizer(_hw(RF_DELAY_ENABLED=False)).process(seq)
    assert off["number_of_blocks"] == on["number_of_blocks"] - 1
    assert min(_ticks(off)) >= MIN_TICKS


def test_start_delay_flag_uses_minimal_nonzero_start(make_seq):
    """START off keeps the (non-emitted) seed block, shrunk to the 20-tick min."""
    seq = make_seq([{"adc": True, "dur": 2e-3}])
    off = Synchronizer(_hw(START_DELAY_ENABLED=False)).process(seq)
    assert off["blocks_duration"][0] == pytest.approx(20 * off["synchro_block_timer"])
    assert min(_ticks(off)) >= MIN_TICKS


def test_all_flags_off_no_artifacts(make_seq):
    seq = make_seq([{"rf": True, "dur": 1e-3},
                    {"dur": 5e-3},
                    {"adc": True, "dur": 2e-3}])
    d = Synchronizer(_hw(TR_DELAY_ENABLED=False,
                         RF_DELAY_ENABLED=False,
                         START_DELAY_ENABLED=False)).process(seq)
    ticks = _ticks(d)
    assert min(ticks) >= MIN_TICKS
    assert max(ticks) <= MAX_TICKS
    assert len(_array_lengths(d)) == 1
