import pytest

from matterlab_balances import SartoriusBalance
from tests.conftest import PORT


@pytest.fixture
def balance_fixture(mock_serial):
    return SartoriusBalance(com_port=PORT)


def test_initialization_defaults(mock_serial, balance_fixture):
    assert isinstance(balance_fixture, SartoriusBalance)
    assert balance_fixture.units == "g"
    assert balance_fixture.device._connection is mock_serial


@pytest.mark.parametrize(
    ("query_return_value", "expected_stable", "expected_weight"),
    [
        ("x 0.123 g", True, 0.123),
        ("x - 0.123 g", True, -0.123),
        ("x 0.123", False, 0.123),
        ("x - 0.123", False, -0.123),
    ],
)
def test_weigh_reads_stable_and_unstable_values(balance_fixture, mocker, query_return_value, expected_stable, expected_weight):
    query_spy = mocker.patch.object(balance_fixture, "query", return_value=query_return_value)

    stable, weight = balance_fixture._weigh()

    assert stable is expected_stable
    assert weight == expected_weight
    query_spy.assert_called_once_with(write_command="\x1bP\r\n", read_delay=0.5)


def test_weigh_stable_returns_immediately(balance_fixture, mocker):
    weigh_spy = mocker.patch.object(balance_fixture, "_weigh", return_value=(True, 0.123))

    assert balance_fixture._weigh_stable() == 0.123
    weigh_spy.assert_called_once_with()


def test_weigh_stable_retries_until_stable(balance_fixture, mocker):
    mocker.patch("matterlab_balances.sartorius_balance.time.sleep")
    weigh_spy = mocker.patch.object(balance_fixture, "_weigh", side_effect=[(False, 0.123), (False, 0.123), (True, 0.123)])

    assert balance_fixture._weigh_stable(max_tries=3, wait_time=0.1) == 0.123
    assert weigh_spy.call_count == 3


def test_weigh_stable_raises_when_never_stable(balance_fixture, mocker):
    mocker.patch("matterlab_balances.sartorius_balance.time.sleep")
    mocker.patch.object(balance_fixture, "_weigh", return_value=(False, 0.123))

    with pytest.raises(IOError, match="Could not get a stable balance reading."):
        balance_fixture._weigh_stable(max_tries=3, wait_time=0.1)


def test_weigh_routes_to_stable_or_unstable_path(balance_fixture, mocker):
    stable_spy = mocker.patch.object(balance_fixture, "_weigh_stable", return_value=0.5)
    weigh_spy = mocker.patch.object(balance_fixture, "_weigh", return_value=(False, 0.25))

    assert balance_fixture.weigh(stable=True) == 0.5
    assert balance_fixture.weigh(stable=False) == 0.25

    stable_spy.assert_called_once_with()
    weigh_spy.assert_called_once_with()


def test_tare_writes_tare_command(balance_fixture, mocker):
    mocker.patch("matterlab_balances.sartorius_balance.time.sleep")
    write_spy = mocker.patch.object(balance_fixture, "write")

    balance_fixture._tare()

    write_spy.assert_called_once_with("\x1bT\r\n")


def test_tare_stable_returns_when_weight_is_within_tolerance(balance_fixture, mocker):
    mocker.patch("matterlab_balances.sartorius_balance.time.sleep")
    tare_spy = mocker.patch.object(balance_fixture, "_tare")
    mocker.patch.object(balance_fixture, "_weigh_stable", return_value=0.0)

    assert balance_fixture._tare_stable() is True
    tare_spy.assert_called_once_with()


def test_tare_stable_retries_until_tolerance_is_met(balance_fixture, mocker):
    mocker.patch("matterlab_balances.sartorius_balance.time.sleep")
    tare_spy = mocker.patch.object(balance_fixture, "_tare")
    mocker.patch.object(balance_fixture, "_weigh_stable", side_effect=[0.5, 0.5, 0.0])

    assert balance_fixture._tare_stable(max_tries=3, wait_time=0.1, tolerance=0.01) is True
    assert tare_spy.call_count == 3


def test_tare_stable_raises_when_tare_never_settles(balance_fixture, mocker):
    mocker.patch("matterlab_balances.sartorius_balance.time.sleep")
    tare_spy = mocker.patch.object(balance_fixture, "_tare")
    mocker.patch.object(balance_fixture, "_weigh_stable", return_value=0.5)

    with pytest.raises(IOError, match="Could not get the balance to tare reliably."):
        balance_fixture._tare_stable(max_tries=3, wait_time=0.1, tolerance=0.01)

    assert tare_spy.call_count == 4


@pytest.mark.parametrize(("stable", "expected_method"), [(False, "_tare"), (True, "_tare_stable")])
def test_tare_routes_to_requested_mode(balance_fixture, mocker, stable, expected_method):
    method_spy = mocker.patch.object(balance_fixture, expected_method)

    balance_fixture.tare(stable=stable)

    method_spy.assert_called_once_with()
