import asyncio
import json
from contextlib import asynccontextmanager
from http import HTTPStatus

import pytest

import pipelet_ocpp_broker as broker


class _DummyCursor:
    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        return False

    async def execute(self, *args, **kwargs):  # pragma: no cover - helper
        return None

    async def fetchall(self):
        return []

    async def fetchone(self):
        return None


class _DummyConnection:
    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        return False

    def cursor(self):
        return _DummyCursor()

    async def commit(self):
        return None

    def close(self):  # pragma: no cover - helper
        return None


@asynccontextmanager
async def _fake_db_connection(*args, **kwargs):
    yield _DummyConnection()


@pytest.fixture(autouse=True)
def _reset_state(monkeypatch):
    broker.PENDING_STARTS.clear()
    broker.ACTIVE_TRANSACTIONS.clear()
    broker.LATEST_CONNECTOR_METERS.clear()
    broker.CONNECTOR_METER_HISTORY.clear()
    broker.CONNECTION_SUBPROTOCOLS.clear()
    broker.ACTIVE_CONNECTION_IDS.clear()
    original_client = broker.HUBJECT_CLIENT
    broker.HUBJECT_CLIENT = None

    async def _always_false(_station_id):
        return False

    monkeypatch.setattr(broker, "db_connection", _fake_db_connection)
    monkeypatch.setattr(broker, "publish_message_via_mqtt", lambda *args, **kwargs: None)
    monkeypatch.setattr(broker, "is_station_oicp_enabled", _always_false)
    yield
    broker.PENDING_STARTS.clear()
    broker.ACTIVE_TRANSACTIONS.clear()
    broker.LATEST_CONNECTOR_METERS.clear()
    broker.CONNECTOR_METER_HISTORY.clear()
    broker.CONNECTION_SUBPROTOCOLS.clear()
    broker.ACTIVE_CONNECTION_IDS.clear()
    broker.HUBJECT_CLIENT = original_client


async def _noop_async(*args, **kwargs):
    return None


@pytest.mark.asyncio
async def test_transaction_event_started(monkeypatch):
    station = "ocpp201/CS001"
    connection_id = 7
    broker.CONNECTION_SUBPROTOCOLS[(station, connection_id)] = "ocpp2.0.1"
    broker.ACTIVE_CONNECTION_IDS[station] = connection_id

    recorded_updates: list[dict] = []
    recorded_store: list[tuple] = []

    async def fake_update(payload):
        recorded_updates.append(payload)

    async def fake_store(station_id, connector_id, meter_value, timestamp):
        recorded_store.append((station_id, connector_id, meter_value, timestamp))

    monkeypatch.setattr(broker, "notify_external_update_transaction", fake_update)
    monkeypatch.setattr(broker, "store_last_meter_reading", fake_store)
    monkeypatch.setattr(broker, "notify_external_stop_transaction", _noop_async)
    monkeypatch.setattr(broker.ocpi_cdr_forwarder, "send_cdr", _noop_async)

    message = [
        2,
        "uid-start",
        "TransactionEvent",
        {
            "eventType": "Started",
            "timestamp": "2023-07-05T12:00:00Z",
            "transactionInfo": {"transactionId": "tx-1"},
            "idToken": {"idToken": "ABC123", "type": "ISO14443"},
            "evse": {"id": 1, "connectorId": 2},
            "meterValue": [
                {
                    "timestamp": "2023-07-05T12:00:00Z",
                    "sampledValue": [
                        {"value": "1234", "measurand": "Energy.Active.Import.Register"}
                    ],
                }
            ],
        },
    ]

    await broker.log_message(station, "client_to_server", json.dumps(message), connection_id)
    await asyncio.sleep(0)

    assert "tx-1" in broker.ACTIVE_TRANSACTIONS
    start_info = broker.ACTIVE_TRANSACTIONS["tx-1"]
    assert start_info["meterStartWh"] == 1234.0
    assert start_info["evseId"] == 1
    assert broker.LATEST_CONNECTOR_METERS["CS001"][2]["Energy.Active.Import.Register"] == 1234.0
    assert recorded_updates and recorded_updates[0]["transactionId"] == "tx-1"
    assert recorded_store and recorded_store[0][1] == 2


@pytest.mark.asyncio
async def test_transaction_event_ended(monkeypatch):
    station = "ocpp201/CS002"
    connection_id = 8
    broker.CONNECTION_SUBPROTOCOLS[(station, connection_id)] = "ocpp2.0.1"
    broker.ACTIVE_CONNECTION_IDS[station] = connection_id

    start_info = {
        "stationId": "CS002",
        "connectorId": 3,
        "evseId": 5,
        "idToken": "ABC123",
        "sessionStartTimestamp": "2023-07-05T12:00:00Z",
        "meterStartWh": 500.0,
    }
    broker.ACTIVE_TRANSACTIONS["tx-9"] = start_info.copy()

    recorded_stops: list[dict] = []
    recorded_cdr: list[tuple] = []

    async def fake_stop(payload):
        recorded_stops.append(payload)

    async def fake_cdr(station_id, start_payload, stop_payload):
        recorded_cdr.append((station_id, start_payload, stop_payload))

    monkeypatch.setattr(broker, "notify_external_stop_transaction", fake_stop)
    monkeypatch.setattr(broker.ocpi_cdr_forwarder, "send_cdr", fake_cdr)
    monkeypatch.setattr(broker, "store_last_meter_reading", _noop_async)
    monkeypatch.setattr(broker, "notify_external_update_transaction", _noop_async)

    message = [
        2,
        "uid-end",
        "TransactionEvent",
        {
            "eventType": "Ended",
            "timestamp": "2023-07-05T12:10:00Z",
            "transactionInfo": {"transactionId": "tx-9", "stoppedReason": "EVDisconnected"},
            "idToken": {"idToken": "ABC123"},
            "evse": {"id": 5, "connectorId": 3},
            "meterValue": [
                {
                    "timestamp": "2023-07-05T12:10:00Z",
                    "sampledValue": [
                        {"value": "1500", "measurand": "Energy.Active.Import.Register"}
                    ],
                }
            ],
        },
    ]

    await broker.log_message(station, "client_to_server", json.dumps(message), connection_id)
    await asyncio.sleep(0)

    assert "tx-9" not in broker.ACTIVE_TRANSACTIONS
    assert recorded_stops and recorded_stops[0]["transactionId"] == "tx-9"
    assert recorded_stops[0]["evseId"] == 5
    assert recorded_stops[0]["meterStopWh"] == 1500.0
    assert recorded_cdr and recorded_cdr[0][0] == "CS002"
    assert recorded_cdr[0][2]["meterStop"] == 1500.0


@pytest.mark.asyncio
async def test_connected_wallboxes_includes_transaction_id(monkeypatch):
    monkeypatch.setattr(broker, "STATION_PATHS", {"/ocpp201/CS003": {}})
    monkeypatch.setattr(broker, "ACTIVE_CLIENTS", {})
    monkeypatch.setattr(
        broker,
        "LATEST_CONNECTOR_STATUS",
        {"CS003": {"2": {"status": "Charging"}}},
    )
    monkeypatch.setattr(broker, "LATEST_CONNECTOR_METERS", {})
    monkeypatch.setattr(broker, "CONNECTOR_METER_HISTORY", {})

    broker.ACTIVE_TRANSACTIONS["tx-active"] = {
        "stationId": "CS003",
        "connectorId": "2",
        "evseId": "5",
        "transactionId": "tx-active",
    }

    status, _headers, body = await broker.process_request(
        "/connectedWallboxes", {}
    )
    assert status == HTTPStatus.OK

    payload = json.loads(body.decode("utf-8"))
    wallboxes = payload.get("wallboxes", [])
    assert wallboxes
    connectors = wallboxes[0].get("connectors", [])
    assert connectors
    connector_entry = connectors[0]
    assert connector_entry.get("connectorId") == 2
    assert connector_entry.get("transactionId") == "tx-active"

    broker.ACTIVE_TRANSACTIONS.pop("tx-active", None)

    status2, _headers2, body2 = await broker.process_request(
        "/connectedWallboxes", {}
    )
    assert status2 == HTTPStatus.OK

    payload2 = json.loads(body2.decode("utf-8"))
    wallboxes2 = payload2.get("wallboxes", [])
    assert wallboxes2
    connectors2 = wallboxes2[0].get("connectors", [])
    assert connectors2
    assert "transactionId" not in connectors2[0]


@pytest.mark.asyncio
async def test_connected_wallboxes_includes_session_energy(monkeypatch):
    monkeypatch.setattr(broker, "STATION_PATHS", {"/ocpp201/CS003": {}})
    monkeypatch.setattr(broker, "ACTIVE_CLIENTS", {})
    monkeypatch.setattr(
        broker,
        "LATEST_CONNECTOR_STATUS",
        {"CS003": {"2": {"status": "Charging"}}},
    )
    meter_payload = {
        "Energy.Active.Import.Register": 150.0,
        "timestamp": "2023-07-05T12:05:00Z",
    }
    monkeypatch.setattr(
        broker,
        "LATEST_CONNECTOR_METERS",
        {"CS003": {"2": meter_payload}},
    )
    monkeypatch.setattr(broker, "CONNECTOR_METER_HISTORY", {})

    broker.ACTIVE_TRANSACTIONS["tx-energy"] = {
        "stationId": "CS003",
        "connectorId": "2",
        "transactionId": "tx-energy",
        "meterStartWh": 100.0,
        "meterLastWh": 140.0,
    }

    status, _headers, body = await broker.process_request(
        "/connectedWallboxes", {}
    )
    assert status == HTTPStatus.OK

    payload = json.loads(body.decode("utf-8"))
    wallboxes = payload.get("wallboxes", [])
    assert wallboxes
    connectors = wallboxes[0].get("connectors", [])
    assert connectors
    connector_entry = connectors[0]
    assert connector_entry.get("meterStartWh") == pytest.approx(100.0)
    assert connector_entry.get("meterLastWh") == pytest.approx(140.0)
    assert connector_entry.get("sessionEnergyDeltaWh") == pytest.approx(40.0)

    broker.ACTIVE_TRANSACTIONS.pop("tx-energy", None)


@pytest.mark.asyncio
async def test_active_sessions_includes_session_energy(monkeypatch):
    meter_payload = {
        "Energy.Active.Import.Register": 260.0,
        "timestamp": "2023-07-05T12:10:00Z",
    }

    broker.ACTIVE_TRANSACTIONS["tx-session"] = {
        "transactionId": "tx-session",
        "stationId": "CS010",
        "connectorId": "3",
        "meterStartWh": 200.0,
    }

    monkeypatch.setattr(
        broker, "LATEST_CONNECTOR_METERS", {"CS010": {"3": meter_payload}}
    )

    status, _headers, body = await broker.process_request(
        "/activeSessions", {}
    )
    assert status == HTTPStatus.OK

    payload = json.loads(body.decode("utf-8"))
    sessions = payload.get("sessions", [])
    assert sessions
    entry = sessions[0]
    assert entry.get("meterStartWh") == pytest.approx(200.0)
    assert entry.get("meterLastWh") == pytest.approx(260.0)
    assert entry.get("sessionEnergyDeltaWh") == pytest.approx(60.0)

    broker.ACTIVE_TRANSACTIONS.pop("tx-session", None)


@pytest.mark.asyncio
async def test_transaction_event_ended_triggers_hubject(monkeypatch):
    station = "ocpp201/CS010"
    connection_id = 12
    broker.CONNECTION_SUBPROTOCOLS[(station, connection_id)] = "ocpp2.0.1"
    broker.ACTIVE_CONNECTION_IDS[station] = connection_id

    start_info = {
        "stationId": "CS010",
        "connectorId": 1,
        "evseId": 2,
        "idToken": "DEADBEEF",
        "sessionStartTimestamp": "2023-07-05T12:00:00Z",
        "meterStartWh": 100.0,
    }
    broker.ACTIVE_TRANSACTIONS["tx-42"] = start_info.copy()

    recorded_cdr: list[tuple[str, dict, dict]] = []
    recorded_hubject: list[tuple[str, dict, dict]] = []

    async def fake_cdr(station_id, start_payload, stop_payload):
        recorded_cdr.append((station_id, start_payload, stop_payload))

    async def fake_hubject(station_id, start_payload, stop_payload, *, retries=1):
        recorded_hubject.append((station_id, start_payload, stop_payload))

    async def always_true(_station_id):
        return True

    broker.HUBJECT_CLIENT = object()
    monkeypatch.setattr(broker.ocpi_cdr_forwarder, "send_cdr", fake_cdr)
    monkeypatch.setattr(broker, "is_station_oicp_enabled", always_true)
    monkeypatch.setattr(broker, "_push_hubject_cdr_with_retry", fake_hubject)
    monkeypatch.setattr(broker, "notify_external_stop_transaction", _noop_async)
    monkeypatch.setattr(broker, "store_last_meter_reading", _noop_async)
    monkeypatch.setattr(broker, "notify_external_update_transaction", _noop_async)

    message = [
        2,
        "uid-end-hubject",
        "TransactionEvent",
        {
            "eventType": "Ended",
            "timestamp": "2023-07-05T12:05:00Z",
            "transactionInfo": {"transactionId": "tx-42"},
            "meterValue": [
                {
                    "timestamp": "2023-07-05T12:05:00Z",
                    "sampledValue": [
                        {"value": "250", "measurand": "Energy.Active.Import.Register"}
                    ],
                }
            ],
        },
    ]

    await broker.log_message(station, "client_to_server", json.dumps(message), connection_id)

    assert recorded_cdr and recorded_hubject
    hubject_entry = recorded_hubject[0]
    assert hubject_entry[0] == "CS010"
    assert hubject_entry[2]["meterStop"] == 250.0


@pytest.mark.asyncio
async def test_stop_transaction_triggers_hubject(monkeypatch):
    station = "ocpp16/CS020"
    station_short = "CS020"
    connection_id = 15
    broker.CONNECTION_SUBPROTOCOLS[(station, connection_id)] = "ocpp1.6"
    broker.ACTIVE_CONNECTION_IDS[station] = connection_id

    start_info = {
        "stationId": station_short,
        "connectorId": 1,
        "idToken": "FFEE",
        "sessionStartTimestamp": "2023-07-05T11:55:00Z",
        "meterStartWh": 10.0,
    }
    broker.ACTIVE_TRANSACTIONS["99"] = start_info.copy()

    recorded_cdr: list[tuple[str, dict, dict]] = []
    recorded_hubject: list[tuple[str, dict, dict]] = []

    async def fake_cdr(station_id, start_payload, stop_payload):
        recorded_cdr.append((station_id, start_payload, stop_payload))

    async def fake_hubject(station_id, start_payload, stop_payload, *, retries=1):
        recorded_hubject.append((station_id, start_payload, stop_payload))

    async def always_true(_station_id):
        return True

    broker.HUBJECT_CLIENT = object()
    monkeypatch.setattr(broker, "notify_external_stop_transaction", _noop_async)
    monkeypatch.setattr(broker.ocpi_cdr_forwarder, "send_cdr", fake_cdr)
    monkeypatch.setattr(broker, "_push_hubject_cdr_with_retry", fake_hubject)
    monkeypatch.setattr(broker, "is_station_oicp_enabled", always_true)

    payload = [
        2,
        "uid-stop",
        "StopTransaction",
        {
            "transactionId": "99",
            "connectorId": 1,
            "meterStop": 123.0,
            "timestamp": "2023-07-05T12:15:00Z",
        },
    ]

    await broker.log_message(station, "client_to_server", json.dumps(payload), connection_id)

    assert recorded_cdr and recorded_hubject
    assert recorded_hubject[0][0] == station_short
    assert recorded_hubject[0][2]["meterStop"] == 123.0
