import json
from contextlib import asynccontextmanager
from datetime import datetime, timezone

import pytest

from hubject_client import (
    HubjectConfigurationError,
    HubjectClient,
    load_hubject_config,
)


@pytest.fixture()
def base_config():
    return {
        "hubject": {
            "api_base_url": "https://example.com/oicp/",
            "authorization": "Bearer token",
            "timeout": 45,
            "client_cert": None,
            "client_key": None,
            "ca_bundle": None,
            "operator_id": "DE*ABC",
        }
    }


def test_load_hubject_config_success(tmp_path, base_config):
    cert_dir = tmp_path / "certs"
    cert_dir.mkdir()
    (cert_dir / "client.pem").write_text("cert", encoding="utf-8")
    (cert_dir / "client.key").write_text("key", encoding="utf-8")
    (cert_dir / "ca.pem").write_text("ca", encoding="utf-8")

    config = json.loads(json.dumps(base_config))
    config["hubject"].update(
        {"client_cert": "client.pem", "client_key": "client.key", "ca_bundle": "ca.pem"}
    )

    cfg = load_hubject_config(config, config_dir=tmp_path, certs_dir=cert_dir)

    assert cfg.api_base_url == "https://example.com/oicp/"
    assert cfg.authorization == "Bearer token"
    assert cfg.timeout == 45
    assert cfg.client_cert == cert_dir / "client.pem"
    assert cfg.client_key == cert_dir / "client.key"
    assert cfg.ca_bundle == cert_dir / "ca.pem"
    assert cfg.operator_id == "DE*ABC"


@pytest.mark.parametrize("missing", ["client_cert", "client_key"])
def test_missing_certificate_files_raise(tmp_path, base_config, missing):
    cert_dir = tmp_path / "certs"
    cert_dir.mkdir()
    (cert_dir / "client.pem").write_text("cert", encoding="utf-8")
    (cert_dir / "client.key").write_text("key", encoding="utf-8")
    (cert_dir / "ca.pem").write_text("ca", encoding="utf-8")

    config = json.loads(json.dumps(base_config))
    config["hubject"].update(
        {"client_cert": "client.pem", "client_key": "client.key", "ca_bundle": "ca.pem"}
    )
    config["hubject"][missing] = "missing-file"

    with pytest.raises(HubjectConfigurationError):
        load_hubject_config(config, config_dir=tmp_path, certs_dir=cert_dir)


@pytest.mark.parametrize(
    "url",
    [
        "",  # empty string
        "ftp://example.com",  # unsupported scheme
        "not a url",  # invalid format
    ],
)
def test_invalid_base_url_raises(tmp_path, base_config, url):
    cert_dir = tmp_path / "certs"
    cert_dir.mkdir()
    (cert_dir / "client.pem").write_text("cert", encoding="utf-8")
    (cert_dir / "client.key").write_text("key", encoding="utf-8")
    (cert_dir / "ca.pem").write_text("ca", encoding="utf-8")

    config = json.loads(json.dumps(base_config))
    config["hubject"].update(
        {"client_cert": "client.pem", "client_key": "client.key", "ca_bundle": "ca.pem"}
    )
    config["hubject"]["api_base_url"] = url

    with pytest.raises(HubjectConfigurationError):
        load_hubject_config(config, config_dir=tmp_path, certs_dir=cert_dir)


def test_client_builds_default_headers(tmp_path, base_config):
    cert_dir = tmp_path / "certs"
    cert_dir.mkdir()
    cfg = load_hubject_config(base_config, config_dir=tmp_path, certs_dir=cert_dir)
    client = HubjectClient(cfg)

    headers = client._prepare_headers({"X-Test": "1"})  # pylint: disable=protected-access
    assert headers["Authorization"] == "Bearer token"
    assert headers["Content-Type"] == "application/json"
    assert headers["X-Test"] == "1"


@pytest.mark.asyncio
async def test_push_cdr_persists_result(monkeypatch, tmp_path, base_config):
    cert_dir = tmp_path / "certs"
    cert_dir.mkdir()
    cfg = load_hubject_config(base_config, config_dir=tmp_path, certs_dir=cert_dir)
    client = HubjectClient(cfg)

    captured_requests: list[tuple[str, dict]] = []

    async def fake_request(
        self,
        operation,
        payload,
        *,
        method="POST",
        headers=None,
        return_raw_text=False,
    ):
        captured_requests.append((operation, payload))
        return {"status": "ok"}, 200, json.dumps({"status": "ok"})

    monkeypatch.setattr(
        client,
        "_request",
        fake_request.__get__(client, HubjectClient),
    )

    class DummyCursor:
        def __init__(self) -> None:
            self.executed: list[tuple[str, tuple]] = []

        async def __aenter__(self):
            return self

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

        async def execute(self, query, params=None):
            self.executed.append((query.strip(), params))

    class DummyConnection:
        def __init__(self) -> None:
            self.cursor_instance = DummyCursor()
            self.commits = 0

        def cursor(self):
            return self.cursor_instance

        async def commit(self):
            self.commits += 1

        def close(self):
            return None

    dummy_conn = DummyConnection()

    @asynccontextmanager
    async def fake_db_connection():
        yield dummy_conn

    monkeypatch.setattr("hubject_client.db_connection", fake_db_connection)

    start_info = {
        "sessionStartTimestamp": "2023-07-05T12:00:00Z",
        "meterStartWh": 10.0,
        "idToken": "ABC123",
    }
    stop_info = {
        "transactionId": "tx-100",
        "timestamp": "2023-07-05T12:15:00Z",
        "meterStop": 42.5,
    }

    success, status, response = await client.push_cdr("CS100", start_info, stop_info)

    assert success is True
    assert status == 200
    assert response == {"status": "ok"}
    assert captured_requests
    operation, payload = captured_requests[0]
    assert operation == "cdrmgmt/v22/operators/DE-ABC/charge-detail-record"
    assert payload["SessionID"] == "tx-100"
    assert payload["MeterValueEnd"] == 42.5
    assert payload["HubOperatorID"] == "DE*ABC"

    insert_calls = [
        entry
        for entry in dummy_conn.cursor_instance.executed
        if entry[0].startswith("INSERT INTO op_hubject_exports")
    ]
    assert insert_calls
    inserted_params = insert_calls[0][1]
    assert inserted_params[0] == "CS100"
    assert inserted_params[1] == "tx-100"


@pytest.mark.asyncio
async def test_set_evse_status_builds_payload(monkeypatch, tmp_path, base_config):
    base_config["hubject"]["operator_id"] = "DE*ABC"
    cert_dir = tmp_path / "certs"
    cert_dir.mkdir()
    cfg = load_hubject_config(base_config, config_dir=tmp_path, certs_dir=cert_dir)
    client = HubjectClient(cfg)

    captured: list[tuple[str, dict[str, object]]] = []

    async def fake_request(
        self,
        operation,
        payload,
        *,
        method="POST",
        headers=None,
        return_raw_text=False,
    ):
        captured.append((operation, payload))
        return {"status": "ok"}, 201, json.dumps({"status": "ok"})

    monkeypatch.setattr(
        client,
        "_request",
        fake_request.__get__(client, HubjectClient),
    )

    ts = datetime(2023, 7, 5, 12, 0, tzinfo=timezone.utc)

    data, status_code, raw_body = await client.set_evse_available(
        "DE*PIP*E123",
        timestamp=ts,
    )

    assert status_code == 201
    assert data == {"status": "ok"}
    assert json.loads(raw_body)["status"] == "ok"

    assert captured
    operation, payload = captured[0]
    assert operation == "/evse/status/record"
    assert payload["OperatorID"] == "DE*ABC"
    record = payload["EVSEStatusRecords"][0]
    assert record["EvseID"] == "DE*PIP*E123"
    assert record["EvseStatus"] == "Available"
    assert record["LastUpdate"] == ts.isoformat().replace("+00:00", "Z")

    captured.clear()

    await client.set_evse_occupied("DE*PIP*E123", timestamp=ts)

    assert captured
    _, payload2 = captured[0]
    assert payload2["EVSEStatusRecords"][0]["EvseStatus"] == "Occupied"
