import asyncio
import json
from datetime import datetime, timedelta
from decimal import Decimal

import pytest

import pipelet_ocpp_server as srv

class FakeCursor:
    def __init__(self, results):
        self.results = results
        self.index = 0
        self.current = None
        self.executed = []
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc, tb):
        pass
    def execute(self, query, params):
        self.executed.append((query, params))
        if self.index < len(self.results):
            self.current = self.results[self.index]
            self.index += 1
        else:
            self.current = None
    def fetchone(self):
        if isinstance(self.current, list):
            return self.current[0] if self.current else None
        return self.current
    def fetchall(self):
        if isinstance(self.current, list):
            return self.current
        return []

class FakeConn:
    def __init__(self, results):
        self.results = results
        self.cur = None
    def cursor(self):
        self.cur = FakeCursor(self.results)
        return self.cur
    def close(self):
        pass

@pytest.mark.asyncio
async def test_check_rfid_authorization_blocked(monkeypatch):
    def fake_conn():
        return FakeConn([None, None, None, 1])
    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    status = await srv.check_rfid_authorization("cp1", "card1")
    assert status == "Blocked"

@pytest.mark.asyncio
async def test_check_rfid_authorization_accepted(monkeypatch):
    def fake_conn():
        return FakeConn([None, None, 1])
    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    status = await srv.check_rfid_authorization("cp1", "card1")
    assert status == "Accepted"

@pytest.mark.asyncio
async def test_check_rfid_authorization_invalid(monkeypatch):
    def fake_conn():
        return FakeConn([None, None, None, None, None, []])
    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    status = await srv.check_rfid_authorization("cp1", "card1")
    assert status == "Invalid"


@pytest.mark.asyncio
async def test_check_rfid_authorization_emsp_token(monkeypatch):
    emsp_row = {"uid": "CARD1", "valid": 1, "whitelist": "ALLOWED"}

    def fake_conn():
        return FakeConn([None, emsp_row])

    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    status = await srv.check_rfid_authorization("cp1", "card1")
    assert status == "Accepted"


@pytest.mark.asyncio
async def test_check_rfid_authorization_emsp_blocked(monkeypatch):
    emsp_row = {"uid": "CARD1", "valid": 0, "whitelist": "BLOCKED"}

    def fake_conn():
        return FakeConn([None, emsp_row])

    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    status = await srv.check_rfid_authorization("cp1", "card1")
    assert status == "Blocked"


@pytest.mark.asyncio
async def test_check_rfid_authorization_normalizes_chargepoint_id(monkeypatch):
    conn = FakeConn([None, None, None, None, None, [], 1])

    def fake_conn():
        return conn

    factory = DummySessionFactory(
        [DummyResponse(json_data={"AuthorizationStatus": "Approved"})]
    )

    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    monkeypatch.setattr(srv, "ClientSession", factory)
    monkeypatch.setitem(
        srv.CONFIG,
        "hubject_api",
        {"host": "relay.example", "port": 9443, "ocpp_timeout": 5, "ocpp_verify_tls": False},
    )

    raw_station_id = "ws://example.com/ocpp/TACW2244321T3066"
    status = await srv.check_rfid_authorization(raw_station_id, "card1")

    assert status == "Accepted"
    assert factory.post_calls
    call = factory.post_calls[0]
    assert "relay.example:9443" in call["url"]
    assert call["json"]["EVSEId"] == "TACW2244321T3066"
    assert call["url"].endswith("/Authorization/AuthorizeStart")
    if conn.cur and conn.cur.executed:
        normalized = "TACW2244321T3066"
        chargepoint_queries = [
            params
            for query, params in conn.cur.executed
            if "chargepoint_id" in query.lower()
        ]
        assert chargepoint_queries
        for params in chargepoint_queries:
            assert normalized in params


@pytest.mark.asyncio
async def test_check_rfid_authorization_global(monkeypatch):
    conn = FakeConn([None, None, None, {"status": "accepted"}])
    def fake_conn():
        return conn
    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    raw_uuid = " card1 "
    status = await srv.check_rfid_authorization("cp1", raw_uuid)
    assert status == "Accepted"
    if conn.cur and len(conn.cur.executed) >= 3:
        passed_uuid = conn.cur.executed[2][1][0]
        normalizer = None
        for name in (
            "normalize_card_uuid",
            "normalize_uuid",
            "normalize_card_id",
            "normalize_tag",
            "normalize_rfid",
        ):
            normalizer = getattr(srv, name, None)
            if normalizer:
                break
        if normalizer:
            assert passed_uuid == normalizer(raw_uuid)
        else:
            assert passed_uuid == raw_uuid.strip().upper()


@pytest.mark.asyncio
async def test_check_rfid_authorization_error(monkeypatch):
    def fake_conn():
        raise Exception("db error")
    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    status = await srv.check_rfid_authorization("cp1", "card1")
    assert status == "Error"


@pytest.mark.asyncio
async def test_remote_start_wildcard_accepts_unknown_rfid(monkeypatch):
    srv._remote_api_authorizations.clear()

    def fake_conn():  # pragma: no cover - should not be called when wildcard matches
        raise AssertionError("database should not be queried for wildcard authorization")

    monkeypatch.setattr(srv, "get_db_conn", fake_conn)

    srv.register_remote_api_authorization(
        "cp-wild", "REMOTE", allow_any_rfid=True
    )

    status_unknown = await srv.check_rfid_authorization("cp-wild", "mystery-card")
    assert status_unknown == "Accepted"

    status_missing = await srv.check_rfid_authorization("cp-wild", "")
    assert status_missing == "Accepted"

    srv._remote_api_authorizations.clear()


@pytest.mark.asyncio
async def test_check_rfid_authorization_voucher_accept(monkeypatch):
    voucher_row = {
        "id": 1,
        "rfid_uuid": "CARD1",
        "energy_kwh": Decimal("5"),
        "energy_used_wh": 500,
        "valid_until": datetime.now() + timedelta(days=1),
        "allowed_chargepoints": "CP1,OTHER",
    }

    def fake_conn():
        return FakeConn([voucher_row])

    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    status = await srv.check_rfid_authorization("cp1", "card1")

    assert status == "Accepted"


@pytest.mark.asyncio
async def test_check_rfid_authorization_voucher_energy_exhausted(monkeypatch):
    voucher_row = {
        "id": 1,
        "rfid_uuid": "CARD1",
        "energy_kwh": Decimal("1"),
        "energy_used_wh": 1000,
        "valid_until": datetime.now() + timedelta(days=1),
        "allowed_chargepoints": "CP1",
    }

    def fake_conn():
        return FakeConn([voucher_row])

    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    status = await srv.check_rfid_authorization("cp1", "card1")

    assert status == "Expired"


@pytest.mark.asyncio
async def test_check_rfid_authorization_voucher_not_allowed_for_station(monkeypatch):
    voucher_row = {
        "id": 1,
        "rfid_uuid": "CARD1",
        "energy_kwh": Decimal("3"),
        "energy_used_wh": 0,
        "valid_until": datetime.now() + timedelta(days=1),
        "allowed_chargepoints": "OTHER-STATION",
    }

    def fake_conn():
        return FakeConn([voucher_row])

    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    status = await srv.check_rfid_authorization("cp1", "card1")

    assert status == "Blocked"


class DummyResponse:
    def __init__(self, *, status=200, json_data=None, text_data=None, json_exception=None):
        self.status = status
        self._json_data = json_data
        self._json_exception = json_exception
        if text_data is not None:
            self._text_data = text_data
        elif json_data is not None:
            try:
                self._text_data = json.dumps(json_data)
            except TypeError:
                self._text_data = ""
        else:
            self._text_data = ""

    async def __aenter__(self):
        return self

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

    async def text(self):
        return self._text_data

    async def json(self, content_type=None):
        if self._json_exception is not None:
            raise self._json_exception
        return self._json_data


class DummySession:
    def __init__(self, factory, responses):
        self._factory = factory
        self._responses = list(responses)

    async def __aenter__(self):
        return self

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

    def post(self, url, json=None, ssl=None, headers=None):
        self._factory.post_calls.append(
            {"url": url, "json": json, "ssl": ssl, "headers": headers}
        )
        if not self._responses:
            raise AssertionError("No dummy response configured for Hubject request")
        return self._responses.pop(0)


class DummySessionFactory:
    def __init__(self, responses):
        self._responses = responses
        self.calls = []
        self.post_calls = []

    def __call__(self, *args, **kwargs):
        self.calls.append((args, kwargs))
        return DummySession(self, self._responses)


@pytest.mark.asyncio
async def test_check_rfid_authorization_hubject_approved(monkeypatch):
    conn = FakeConn([None, None, None, None, None, [], 1])

    def fake_conn():
        return conn

    factory = DummySessionFactory(
        [DummyResponse(json_data={"AuthorizationStatus": "Approved"})]
    )
    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    monkeypatch.setattr(srv, "ClientSession", factory)
    monkeypatch.setitem(
        srv.CONFIG,
        "hubject_api",
        {"ocpp_base_url": "http://relay.example", "ocpp_timeout": 5, "ocpp_verify_tls": False},
    )

    status = await srv.check_rfid_authorization("cp1", "card1")

    assert status == "Accepted"
    assert factory.calls
    assert factory.post_calls
    call = factory.post_calls[0]
    assert call["url"].endswith("/Authorization/AuthorizeStart")
    assert call["json"] == {"EVSEId": "cp1", "RFID": "CARD1"}


@pytest.mark.asyncio
async def test_check_rfid_authorization_hubject_blocked(monkeypatch):
    conn = FakeConn([None, None, None, None, [], 1])

    def fake_conn():
        return conn

    factory = DummySessionFactory(
        [DummyResponse(json_data={"AuthorizationStatus": "Blocked"})]
    )
    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    monkeypatch.setattr(srv, "ClientSession", factory)
    monkeypatch.setitem(
        srv.CONFIG,
        "hubject_api",
        {"ocpp_base_url": "http://relay.example", "ocpp_timeout": 5, "ocpp_verify_tls": False},
    )

    status = await srv.check_rfid_authorization("cp1", "card1")

    assert status == "Blocked"


@pytest.mark.asyncio
async def test_check_rfid_authorization_hubject_timeout(monkeypatch):
    conn = FakeConn([None, None, None, None, [], 1])

    def fake_conn():
        return conn

    async def fake_authorize(*args, **kwargs):
        raise asyncio.TimeoutError()

    async def fake_query(evse_id, rfid):
        return await fake_authorize(evse_id, rfid)

    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    monkeypatch.setattr(srv, "_query_hubject_api_authorize_start", fake_query)

    status = await srv.check_rfid_authorization("cp1", "card1")

    assert status == "Invalid"


@pytest.mark.asyncio
async def test_check_rfid_authorization_hubject_failure(monkeypatch):
    conn = FakeConn([None, None, None, None, [], 1])

    def fake_conn():
        return conn

    factory = DummySessionFactory(
        [DummyResponse(status=502, text_data="Bad Gateway", json_data=None)]
    )
    monkeypatch.setattr(srv, "get_db_conn", fake_conn)
    monkeypatch.setattr(srv, "ClientSession", factory)
    monkeypatch.setitem(
        srv.CONFIG,
        "hubject_api",
        {"ocpp_base_url": "http://relay.example", "ocpp_timeout": 5, "ocpp_verify_tls": False},
    )

    status = await srv.check_rfid_authorization("cp1", "card1")

    assert status == "Invalid"
    assert factory.post_calls

