"""Driver maths and register sequences -- no hardware, no USB."""

import pytest

import rm3100
from conftest import FakeBus


# --------------------------------------------------------------------------
# Gain and resolution
# --------------------------------------------------------------------------

@pytest.mark.parametrize("cycle_count, table_lsb_per_ut", [
    (50, 20), (100, 38), (200, 75),
])
def test_gain_fit_reproduces_table_3_1(cycle_count, table_lsb_per_ut):
    """The fit exists to reproduce Table 3-1's three points within a count."""
    fitted = rm3100.gain_lsb_per_tesla(cycle_count) / rm3100.UT_PER_TESLA
    assert abs(fitted - table_lsb_per_ut) < 1.0


def test_tesla_per_count_inverts_the_gain():
    for cc in (30, 100, 400, 65535):
        assert rm3100.tesla_per_count(cc) * rm3100.gain_lsb_per_tesla(cc) == pytest.approx(1.0)


def test_higher_cycle_count_means_finer_lsb():
    steps = [rm3100.tesla_per_count(cc) for cc in (50, 100, 200, 400)]
    assert steps == sorted(steps, reverse=True)


def test_expected_noise_follows_inverse_sqrt():
    """Table 3-1's 30/20/15 nT at 50/100/200 is a K/sqrt(cc) law."""
    assert rm3100.expected_noise_nt(50) == pytest.approx(29.4, abs=0.7)
    assert rm3100.expected_noise_nt(100) == pytest.approx(20.8, abs=0.9)
    assert rm3100.expected_noise_nt(200) == pytest.approx(14.7, abs=0.4)
    # Quadrupling the cycle count halves the noise.
    assert (rm3100.expected_noise_nt(100)
            == pytest.approx(2 * rm3100.expected_noise_nt(400)))


# --------------------------------------------------------------------------
# Timing model
# --------------------------------------------------------------------------

def test_sample_period_matches_the_documented_model():
    expected = 3 * (100 / rm3100.COUNTS_PER_SECOND + rm3100.AXIS_OVERHEAD_S)
    assert rm3100.sample_period(100) == pytest.approx(expected)
    # The default configuration is quoted at ~282 Hz throughout the docs.
    assert 1 / rm3100.sample_period(100) == pytest.approx(282, abs=1)


def test_integration_time_excludes_overhead():
    assert rm3100.integration_time(100) < rm3100.sample_period(100)
    assert rm3100.integration_time(100) == pytest.approx(3 * 100 / 90000.0)


def test_duty_rises_with_cycle_count():
    """Fixed per-axis overhead is a shrinking share as integration grows."""
    duties = [rm3100.integration_time(cc) / rm3100.sample_period(cc)
              for cc in (50, 100, 200, 800)]
    assert duties == sorted(duties)
    assert duties[-1] < 1.0


@pytest.mark.parametrize("rate", [0.5, 1, 8, 32, 128, 282, 500])
def test_cycle_count_for_rate_inverts_sample_period(rate):
    cc = rm3100.cycle_count_for_rate(rate)
    achieved = 1.0 / rm3100.sample_period(cc)
    # Rounding to an integer cycle count is the only error here.
    assert achieved == pytest.approx(rate, rel=0.01)


def test_cycle_count_for_rate_clamps_to_the_register_width():
    assert rm3100.cycle_count_for_rate(1e9) == 1
    assert rm3100.cycle_count_for_rate(1e-9) == rm3100.MAX_CYCLE_COUNT


# --------------------------------------------------------------------------
# plan(): every branch, since it is the only place configuration is resolved
# --------------------------------------------------------------------------

def test_plan_default_is_the_documented_configuration():
    cfg = rm3100.plan()
    assert cfg.cycle_count == rm3100.DEFAULT_CYCLE_COUNT
    assert cfg.tmrc == rm3100.TMRC_FASTEST
    assert cfg.governed_by == "cycle count"
    assert cfg.predicted_hz == pytest.approx(282, abs=1)
    assert cfg.duty == pytest.approx(0.94, abs=0.01)
    assert cfg.notes == []


def test_plan_from_rate_derives_the_cycle_count():
    cfg = rm3100.plan(rate_hz=50)
    assert cfg.predicted_hz == pytest.approx(50, rel=0.01)
    assert cfg.governed_by == "cycle count"
    assert cfg.notes == []


def test_plan_from_cycle_count_ignores_the_rate_knob():
    cfg = rm3100.plan(cycle_count=400)
    assert cfg.cycle_count == 400
    assert cfg.predicted_hz == pytest.approx(1 / rm3100.sample_period(400))


def test_plan_raises_a_cycle_count_below_the_quantisation_floor():
    cfg = rm3100.plan(cycle_count=5)
    assert cfg.cycle_count == rm3100.MIN_CYCLE_COUNT
    assert any("quantisation floor" in n for n in cfg.notes)


def test_plan_clamps_a_cycle_count_above_the_register_width():
    cfg = rm3100.plan(cycle_count=100_000)
    assert cfg.cycle_count == rm3100.MAX_CYCLE_COUNT


def test_plan_below_the_cycle_count_floor_hands_the_cadence_to_tmrc():
    """Under ~0.46 Hz the 16-bit register runs out and only TMRC can go slower."""
    rate = rm3100.MIN_RATE_BY_CYCLE_COUNT / 2
    cfg = rm3100.plan(rate_hz=rate)
    assert cfg.cycle_count == rm3100.MAX_CYCLE_COUNT
    assert cfg.governed_by == "TMRC"
    assert cfg.tmrc == min(rm3100.TMRC_RATES,
                           key=lambda t: abs(rm3100.TMRC_RATES[t] - rate))
    assert any("below the" in n for n in cfg.notes)


def test_plan_warns_when_tmrc_governs_and_the_sensor_idles():
    """cc=50 can run at 534 Hz; TMRC 0x99 asks for 4.5, so it idles ~99%."""
    cfg = rm3100.plan(cycle_count=50, tmrc=0x99)
    assert cfg.governed_by == "TMRC"
    assert cfg.predicted_hz == pytest.approx(4.5)
    assert cfg.duty < 0.02
    assert any("idle" in n for n in cfg.notes)


def test_plan_notes_a_rate_it_cannot_reach():
    cfg = rm3100.plan(rate_hz=5000)
    assert cfg.predicted_hz < 5000
    assert any("faster than this configuration can reach" in n for n in cfg.notes)


def test_plan_duty_is_integration_over_the_achieved_period():
    cfg = rm3100.plan(cycle_count=200)
    assert cfg.duty == pytest.approx(
        rm3100.integration_time(200) * cfg.predicted_hz)


def test_plan_rejects_an_unknown_tmrc():
    with pytest.raises(KeyError):
        rm3100.plan(tmrc=0x00)


# --------------------------------------------------------------------------
# Measurement decoding -- 24-bit two's complement, big-endian
# --------------------------------------------------------------------------

@pytest.mark.parametrize("raw, expected", [
    (b"\x00\x00\x00" * 3, (0, 0, 0)),
    (b"\x00\x00\x01" * 3, (1, 1, 1)),
    (b"\xff\xff\xff" * 3, (-1, -1, -1)),
    (b"\x7f\xff\xff" * 3, (8388607, 8388607, 8388607)),      # positive rail
    (b"\x80\x00\x00" * 3, (-8388608, -8388608, -8388608)),   # negative rail
])
def test_decode_measurements_sign_extends(raw, expected):
    assert rm3100.decode_measurements(raw) == expected


def test_decode_measurements_keeps_axes_in_order():
    assert rm3100.decode_measurements(
        bytes([0, 0, 1, 0, 0, 2, 255, 255, 255])) == (1, 2, -1)


@pytest.mark.parametrize("length", [0, 8, 10, 27])
def test_decode_measurements_rejects_a_wrong_length(length):
    with pytest.raises(ValueError, match="9 measurement bytes"):
        rm3100.decode_measurements(bytes(length))


# --------------------------------------------------------------------------
# Register access
# --------------------------------------------------------------------------

def test_set_cycle_counts_writes_all_three_axes_big_endian():
    bus = FakeBus()
    sensor = rm3100.RM3100(bus, 0x23)
    sensor.set_cycle_counts(0x0190)
    assert bus.writes == [(0x23, bytes([rm3100.REG_CCX]) + b"\x01\x90" * 3)]
    assert sensor.get_cycle_counts() == (400, 400, 400)


@pytest.mark.parametrize("bad", [-1, 0x10000, 999999])
def test_set_cycle_counts_rejects_out_of_range(bad):
    sensor = rm3100.RM3100(FakeBus(), 0x23)
    with pytest.raises(ValueError, match="outside 0..65535"):
        sensor.set_cycle_counts(bad)


def test_set_rate_rejects_a_value_outside_the_tmrc_table():
    sensor = rm3100.RM3100(FakeBus(), 0x23)
    with pytest.raises(ValueError, match="not one of"):
        sensor.set_rate(0x91)
    sensor.set_rate(rm3100.TMRC_FASTEST)      # a valid one must not raise


def test_configure_accepts_a_correct_hshake_readback():
    bus = FakeBus({rm3100.REG_HSHAKE: 0x1B})
    rm3100.RM3100(bus, 0x23).configure()
    assert bus.registers[rm3100.REG_HSHAKE] == rm3100.HSHAKE_DRDY_ON_READ_ONLY


def test_configure_ignores_the_read_only_nack_bits():
    """Bits 4-6 are NACK status, so they may read back set without meaning failure."""
    bus = FakeBus()
    sensor = rm3100.RM3100(bus, 0x23)

    def read_reg(reg, count=1):
        return bytes([rm3100.HSHAKE_DRDY_ON_READ_ONLY | 0x70]), 0.0, 0.0

    sensor.read_reg = read_reg
    sensor.configure()      # must not raise


def test_configure_raises_when_drc1_did_not_take():
    """Exactly-once sampling depends on DRC1, so a bad readback is fatal."""
    sensor = rm3100.RM3100(FakeBus(), 0x23)
    sensor.read_reg = lambda reg, count=1: (b"\x1b", 0.0, 0.0)
    with pytest.raises(IOError, match="HSHAKE did not take"):
        sensor.configure()


def test_read_reg_prefers_a_combined_transaction():
    bus = FakeBus({rm3100.REG_REVID: rm3100.EXPECTED_REVID})
    sensor = rm3100.RM3100(bus, 0x23)
    assert sensor.revid() == rm3100.EXPECTED_REVID
    # One transaction, not a separate write then read.
    assert len(bus.writes) == 1


def test_read_reg_falls_back_when_the_bus_cannot_do_repeated_start():
    bus = FakeBus({rm3100.REG_REVID: rm3100.EXPECTED_REVID}, combined=False)
    sensor = rm3100.RM3100(bus, 0x23)
    assert sensor.revid() == rm3100.EXPECTED_REVID


def test_read_raw_decodes_from_the_measurement_registers():
    registers = {rm3100.REG_MX + i: b for i, b in enumerate(
        [0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF])}
    sensor = rm3100.RM3100(FakeBus(registers), 0x23)
    counts, mono, wall = sensor.read_raw()
    assert counts == (1, -1, 8388607)
    assert mono > 0 and wall > mono      # the stamp travels with the data


def test_read_measurements_converts_with_the_active_cycle_count():
    registers = {rm3100.REG_MX + i: b for i, b in enumerate(
        [0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])}
    sensor = rm3100.RM3100(FakeBus(registers), 0x23)
    sensor.cycle_count = 100
    counts, tesla = sensor.read_measurements()
    assert counts[0] == 0x26
    # 38 LSB/uT at cc=100, so 0x26 counts is very close to 1 uT.
    assert tesla[0] == pytest.approx(1e-6, rel=0.01)


def test_data_ready_reads_the_drdy_bit():
    sensor = rm3100.RM3100(FakeBus({rm3100.REG_STATUS: 0x80}), 0x23)
    assert sensor.data_ready() is True
    sensor = rm3100.RM3100(FakeBus({rm3100.REG_STATUS: 0x00}), 0x23)
    assert sensor.data_ready() is False


def test_wait_for_data_times_out_without_drdy():
    sensor = rm3100.RM3100(FakeBus({rm3100.REG_STATUS: 0x00}), 0x23)
    assert sensor.wait_for_data(timeout=0.01, interval=0.001) is False


def test_cmm_start_and_stop_write_the_documented_values():
    bus = FakeBus()
    sensor = rm3100.RM3100(bus, 0x23)
    sensor.start_cmm()
    sensor.stop_cmm()
    assert bus.writes == [
        (0x23, bytes([rm3100.REG_CMM, rm3100.CMM_ALL_AXES])),
        (0x23, bytes([rm3100.REG_CMM, rm3100.CMM_OFF])),
    ]


def test_addresses_cover_both_strap_pins():
    assert list(rm3100.RM3100.ADDRESSES) == [0x20, 0x21, 0x22, 0x23]
