from pathlib import Path
from types import SimpleNamespace

import pytest

from matterlab_balances.mt_balance import (
    MTXPRBalance,
    MTXPRBalanceConnectionError,
    MTXPRBalanceDeviceError,
    MTXPRBalanceRequestError,
    WeighingCaptureMode,
)


@pytest.fixture
def mt_fixture():
    return MTXPRBalance(connect_on_init=False)


def test_initialization_defaults(mt_fixture):
    assert mt_fixture.host == "192.168.1.1"
    assert mt_fixture.port == 8002
    assert mt_fixture.client is None
    assert mt_fixture._session_id is None
    assert mt_fixture._active_command_ids == set()


def test_build_wsdl_file_renders_template(tmp_path, mt_fixture):
    template_path = tmp_path / "template.wsdl.jinja2"
    output_path = tmp_path / "generated.wsdl"
    template_path.write_text("http://{{ host }}:{{ port }}/{{ api_path }}", encoding="utf-8")
    mt_fixture.wsdl_template_path = template_path
    mt_fixture.generated_wsdl_path = output_path

    mt_fixture._build_wsdl_file()

    assert output_path.read_text(encoding="utf-8") == "http://192.168.1.1:8002/MT/Laboratory/Balance/XprXsr/V03/MT"


def test_connect_builds_client_and_opens_session(mocker, mt_fixture, tmp_path):
    mt_fixture.generated_wsdl_path = tmp_path / "generated.wsdl"
    build_wsdl_spy = mocker.patch.object(mt_fixture, "_build_wsdl_file")
    open_session_spy = mocker.patch.object(mt_fixture, "open_session")
    client_spy = mocker.patch("matterlab_balances.mt_balance.Client", return_value=SimpleNamespace())

    mt_fixture.connect()

    build_wsdl_spy.assert_called_once_with()
    client_spy.assert_called_once_with(mt_fixture.generated_wsdl_path.as_uri())
    open_session_spy.assert_called_once_with()


def test_request_requires_connected_client(mt_fixture):
    with pytest.raises(MTXPRBalanceConnectionError, match="Client not connected. Call connect\\(\\) first."):
        mt_fixture._request("svc", "method")


def test_request_opens_session_when_needed_and_tracks_command_id(mocker, mt_fixture, success_response):
    method_spy = mocker.MagicMock(return_value=success_response(CommandId=321))
    mt_fixture.client = SimpleNamespace(service={"svc": SimpleNamespace(method=method_spy)})

    def _open_session():
        mt_fixture._session_id = "session-id"

    open_session_spy = mocker.patch.object(mt_fixture, "open_session", side_effect=_open_session)

    response = mt_fixture._request("svc", "method", args=["arg"])

    open_session_spy.assert_called_once_with()
    method_spy.assert_called_once_with("session-id", "arg")
    assert response.CommandId == 321
    assert mt_fixture._active_command_ids == {321}


def test_request_raises_on_non_success_outcome(mt_fixture):
    method = lambda *args: SimpleNamespace(Outcome="Failed", ErrorMessage="bad", ErrorState="Broken")
    mt_fixture.client = SimpleNamespace(service={"svc": SimpleNamespace(method=method)})
    mt_fixture._session_id = "session-id"

    with pytest.raises(MTXPRBalanceRequestError, match="Request svc.method failed."):
        mt_fixture._request("svc", "method")


def test_open_session_decrypts_and_stores_session_id(mocker, mt_fixture, success_response):
    request_spy = mocker.patch.object(
        mt_fixture,
        "_request",
        return_value=success_response(SessionId="enc", Salt="salt"),
    )
    decrypt_spy = mocker.patch.object(mt_fixture, "decrypt_session_id", return_value="session-id")

    mt_fixture.open_session()

    request_spy.assert_called_once_with(mt_fixture.SESSION_SERVICE, "OpenSession", include_session_id=False)
    decrypt_spy.assert_called_once_with("password", "enc", "salt")
    assert mt_fixture._session_id == "session-id"


def test_close_session_clears_active_session(mocker, mt_fixture, success_response):
    request_spy = mocker.patch.object(mt_fixture, "_request", return_value=success_response())
    mt_fixture._session_id = "session-id"

    mt_fixture.close_session()

    request_spy.assert_called_once_with(mt_fixture.SESSION_SERVICE, "CloseSession", args=["session-id"], include_session_id=False)
    assert mt_fixture._session_id is None


def test_tare_and_zero_use_request_helper(mocker, mt_fixture, success_response):
    request_spy = mocker.patch.object(mt_fixture, "_request", return_value=success_response(ErrorState="Undefined"))

    mt_fixture.tare(immediately=True)
    mt_fixture.zero(immediately=False)

    request_spy.assert_any_call(mt_fixture.WEIGHING_SERVICE, "Tare", ["true"])
    request_spy.assert_any_call(mt_fixture.WEIGHING_SERVICE, "Zero", ["false"])


def test_get_weight_returns_value_unit_and_stability(mocker, mt_fixture, success_response):
    weight_sample = SimpleNamespace(
        Status="Ok",
        NetWeight=SimpleNamespace(Value="1.23", Unit="Gram"),
        Stable=True,
    )
    request_spy = mocker.patch.object(mt_fixture, "_request", return_value=success_response(WeightSample=weight_sample))

    value, unit, stable = mt_fixture.get_weight(capture_mode=WeighingCaptureMode.IMMEDIATE, timeout_seconds=30)

    request_spy.assert_called_once_with(
        mt_fixture.WEIGHING_SERVICE,
        "GetWeight",
        ["Immediate", None, None, None, None, None, 30],
    )
    assert value == 1.23
    assert unit == "Gram"
    assert stable is True


def test_get_weight_raises_for_bad_status(mocker, mt_fixture, success_response):
    weight_sample = SimpleNamespace(Status="Overload", NetWeight=SimpleNamespace(Value="1.23", Unit="Gram"), Stable=False)
    mocker.patch.object(mt_fixture, "_request", return_value=success_response(WeightSample=weight_sample))

    with pytest.raises(MTXPRBalanceDeviceError, match="Weight sample status is not Ok: Overload"):
        mt_fixture.get_weight()


def test_context_manager_connects_and_cleans_up(mocker, mt_fixture):
    connect_spy = mocker.patch.object(mt_fixture, "connect")
    cancel_all_spy = mocker.patch.object(mt_fixture, "cancel_all")
    close_session_spy = mocker.patch.object(mt_fixture, "close_session")
    mt_fixture._active_command_ids.add(1)

    with mt_fixture as balance:
        assert balance is mt_fixture

    connect_spy.assert_called_once_with()
    cancel_all_spy.assert_called_once_with()
    close_session_spy.assert_called_once_with()
