from unittest.mock import MagicMock, patch, call
import pytest

from backend.epc_client.epc_client_service import EpcClientService
from datatypes.epc.search import EpcSearchResult
from backend.epc_client.exceptions import EpcNotFoundError, EpcRateLimitError
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from backend.epc_client.tests.conftest import make_search_row


def _mock_response(status_code=200, json_data=None, headers=None):
    resp = MagicMock()
    resp.status_code = status_code
    resp.is_success = 200 <= status_code < 300
    resp.json.return_value = json_data or {}
    resp.text = str(json_data)
    resp.headers = headers or {}
    return resp


# ---------------------------------------------------------------------------
# Test 1: get_by_certificate_number happy path
# ---------------------------------------------------------------------------


def test_get_by_certificate_number_returns_epc_property_data(
    epc_service, rdsap_21_0_1_cert
):
    cert_response = {"data": rdsap_21_0_1_cert}
    with patch("httpx.get", return_value=_mock_response(200, cert_response)):
        result = epc_service.get_by_certificate_number("CERT-001")

    assert isinstance(result, EpcPropertyData)


# ---------------------------------------------------------------------------
# Test 2: get_by_certificate_number 404 → EpcNotFoundError
# ---------------------------------------------------------------------------


def test_get_by_certificate_number_404_raises_not_found(epc_service):
    with patch("httpx.get", return_value=_mock_response(404)):
        with pytest.raises(EpcNotFoundError):
            epc_service.get_by_certificate_number("BAD-CERT")


# ---------------------------------------------------------------------------
# Test 3: 429 retried, succeeds on 3rd attempt
# ---------------------------------------------------------------------------


def test_get_by_certificate_number_retries_on_429_and_succeeds(
    epc_service, rdsap_21_0_1_cert
):
    cert_response = {"data": rdsap_21_0_1_cert}
    responses = [
        _mock_response(429),
        _mock_response(429),
        _mock_response(200, cert_response),
    ]
    with patch("httpx.get", side_effect=responses), patch("time.sleep"):
        result = epc_service.get_by_certificate_number("CERT-001")

    assert isinstance(result, EpcPropertyData)


# ---------------------------------------------------------------------------
# Test 3b: 429 with Retry-After header → sleeps for that value
# ---------------------------------------------------------------------------


def test_429_retry_after_header_drives_sleep_duration(
    epc_service, rdsap_21_0_1_cert
):
    cert_response = {"data": rdsap_21_0_1_cert}
    responses = [
        _mock_response(429, headers={"Retry-After": "7"}),
        _mock_response(200, cert_response),
    ]
    with patch("httpx.get", side_effect=responses), patch(
        "backend.epc_client._retry.time.sleep"
    ) as mock_sleep:
        epc_service.get_by_certificate_number("CERT-001")

    mock_sleep.assert_called_once_with(7.0)


# ---------------------------------------------------------------------------
# Test 3c: 429 without Retry-After → falls back to exponential backoff
# ---------------------------------------------------------------------------


def test_429_without_retry_after_uses_exponential_backoff(
    epc_service, rdsap_21_0_1_cert
):
    cert_response = {"data": rdsap_21_0_1_cert}
    responses = [
        _mock_response(429),
        _mock_response(429),
        _mock_response(200, cert_response),
    ]
    with patch("httpx.get", side_effect=responses), patch(
        "backend.epc_client._retry.time.sleep"
    ) as mock_sleep:
        epc_service.get_by_certificate_number("CERT-001")

    assert mock_sleep.call_args_list == [call(1.0), call(2.0)]


# ---------------------------------------------------------------------------
# Test 3d: malformed Retry-After header → falls back to exponential backoff
# ---------------------------------------------------------------------------


def test_429_malformed_retry_after_falls_back_to_backoff(
    epc_service, rdsap_21_0_1_cert
):
    cert_response = {"data": rdsap_21_0_1_cert}
    responses = [
        _mock_response(429, headers={"Retry-After": "Wed, 21 Oct 2026 07:28:00 GMT"}),
        _mock_response(200, cert_response),
    ]
    with patch("httpx.get", side_effect=responses), patch(
        "backend.epc_client._retry.time.sleep"
    ) as mock_sleep:
        epc_service.get_by_certificate_number("CERT-001")

    mock_sleep.assert_called_once_with(1.0)


# ---------------------------------------------------------------------------
# Test 3e: Retry-After capped by max_backoff to avoid hostile/buggy values
# ---------------------------------------------------------------------------


def test_429_retry_after_capped_by_max_backoff(epc_service, rdsap_21_0_1_cert):
    cert_response = {"data": rdsap_21_0_1_cert}
    responses = [
        _mock_response(429, headers={"Retry-After": "9999"}),
        _mock_response(200, cert_response),
    ]
    with patch("httpx.get", side_effect=responses), patch(
        "backend.epc_client._retry.time.sleep"
    ) as mock_sleep:
        epc_service.get_by_certificate_number("CERT-001")

    mock_sleep.assert_called_once_with(60.0)


# ---------------------------------------------------------------------------
# Test 4: get_by_uprn empty search → None
# ---------------------------------------------------------------------------


def test_get_by_uprn_returns_none_when_no_results(epc_service):
    with patch("httpx.get", return_value=_mock_response(200, {"data": []})):
        result = epc_service.get_by_uprn(100023336956)

    assert result is None


# ---------------------------------------------------------------------------
# Test 5: get_by_uprn multiple results → fetches latest by registration_date
# ---------------------------------------------------------------------------


def test_get_by_uprn_picks_most_recent_certificate(epc_service, rdsap_21_0_1_cert):
    search_rows = [
        make_search_row(cert_num="CERT-OLD", registration_date="2022-01-01"),
        make_search_row(cert_num="CERT-NEW", registration_date="2024-06-01"),
        make_search_row(cert_num="CERT-MID", registration_date="2023-03-15"),
    ]
    cert_response = {"data": rdsap_21_0_1_cert}

    def fake_get(url, params=None, **kwargs):
        if "search" in url:
            return _mock_response(200, {"data": search_rows})
        return _mock_response(200, cert_response)

    with patch("httpx.get", side_effect=fake_get) as mock_get:
        result = epc_service.get_by_uprn(100023336956)

    assert isinstance(result, EpcPropertyData)
    # Second call must be for the most recent cert
    cert_call = mock_get.call_args_list[1]
    assert cert_call.kwargs["params"]["certificate_number"] == "CERT-NEW"


# ---------------------------------------------------------------------------
# Test 6: search_by_postcode returns list[EpcSearchResult]
# ---------------------------------------------------------------------------


def test_search_by_postcode_returns_results(epc_service):
    rows = [
        make_search_row(cert_num="CERT-A", address_line_1="1 High Street"),
        make_search_row(cert_num="CERT-B", address_line_1="2 High Street"),
    ]
    with patch("httpx.get", return_value=_mock_response(200, {"data": rows})):
        results = epc_service.search_by_postcode("SW1A 1AA")

    assert len(results) == 2
    assert all(isinstance(r, EpcSearchResult) for r in results)
    assert results[0].certificate_number == "CERT-A"
    assert results[1].address_line_1 == "2 High Street"


# ---------------------------------------------------------------------------
# Test 7: search_by_postcode 404 → empty list
# ---------------------------------------------------------------------------


def test_search_by_postcode_404_returns_empty_list(epc_service):
    with patch("httpx.get", return_value=_mock_response(404)):
        results = epc_service.search_by_postcode("ZZ9 9ZZ")

    assert results == []
