"""Tests for api.py - API client with httpx mocking."""

from __future__ import annotations

import pytest
import httpx
from pytest_httpx import HTTPXMock

from cli.api import BeaconAPI
from cli.config import Settings
from cli.exceptions import BeaconAPIError, BeaconAuthError, BeaconNotFoundError

from .conftest import SAMPLE_BEACON, SAMPLE_BEACON_LIST, TEST_ORG_ID


BASE_URL = "https://api.test.ethiack.com"


@pytest.fixture()
def api(settings) -> BeaconAPI:
    return BeaconAPI(settings)


# ---------------------------------------------------------------------------
# Organizations
# ---------------------------------------------------------------------------


class TestGetOrganizations:
    def test_returns_list(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/organizations/",
            json={"organizations": [{"id": 1, "name": "Acme"}, {"id": 2, "name": "Beta"}]},
        )
        orgs = api.get_organizations()
        assert len(orgs) == 2
        assert orgs[0]["name"] == "Acme"

    def test_returns_empty_list_when_none(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/organizations/",
            json={"organizations": []},
        )
        assert api.get_organizations() == []

    def test_auth_error_raises(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/organizations/",
            status_code=401,
        )
        from cli.exceptions import BeaconAuthError
        with pytest.raises(BeaconAuthError):
            api.get_organizations()


# ---------------------------------------------------------------------------
# Create
# ---------------------------------------------------------------------------


class TestCreate:
    def test_create_beacon_success(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/",
            json=SAMPLE_BEACON,
            status_code=201,
        )
        result = api.create(name="test-beacon", allowed_cidrs=["192.168.1.0/24"])
        assert result["id"] == "beacon-id-001"
        assert result["wireguard_config"].startswith("[Interface]")

    def test_create_request_body_contains_only_name_and_cidrs(self, api, httpx_mock: HTTPXMock):
        """create() only sends name and allowed_cidrs; DNS is set via update()."""
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/",
            json=SAMPLE_BEACON,
            status_code=201,
        )
        api.create(name="test", allowed_cidrs=["10.0.0.0/8"])
        request = httpx_mock.get_request()
        import json
        body = json.loads(request.content)
        assert set(body.keys()) == {"name", "allowed_cidrs"}
        assert "dns_server" not in body
        assert "interface_name" not in body

    def test_create_returns_401_raises_auth_error(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/",
            status_code=401,
        )
        with pytest.raises(BeaconAuthError):
            api.create("n", ["10.0.0.0/8"])

    def test_create_server_error_raises_api_error(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/",
            json={"detail": "Internal server error"},
            status_code=500,
        )
        with pytest.raises(BeaconAPIError, match="Internal server error"):
            api.create("n", ["10.0.0.0/8"])


# ---------------------------------------------------------------------------
# List
# ---------------------------------------------------------------------------


class TestList:
    def test_list_returns_results(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/?page=1&per_page=20",
            json=SAMPLE_BEACON_LIST,
        )
        result = api.list()
        assert len(result["beacons"]) == 2

    def test_list_with_status_filter(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/?page=1&per_page=20&status=active",
            json={"beacons": [SAMPLE_BEACON]},
        )
        result = api.list(status="active")
        assert len(result["beacons"]) == 1


# ---------------------------------------------------------------------------
# Get
# ---------------------------------------------------------------------------


class TestGet:
    def test_get_existing_beacon(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/beacon-id-001",
            json=SAMPLE_BEACON,
        )
        result = api.get("beacon-id-001")
        assert result["name"] == "test-beacon"

    def test_get_nonexistent_raises_not_found(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/nonexistent",
            status_code=404,
        )
        with pytest.raises(BeaconNotFoundError):
            api.get("nonexistent")


# ---------------------------------------------------------------------------
# Update
# ---------------------------------------------------------------------------


class TestUpdate:
    def test_update_name(self, api, httpx_mock: HTTPXMock):
        updated = {**SAMPLE_BEACON, "name": "new-name"}
        httpx_mock.add_response(
            method="PATCH",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/beacon-id-001",
            json=updated,
        )
        result = api.update("beacon-id-001", name="new-name")
        assert result["name"] == "new-name"


# ---------------------------------------------------------------------------
# Delete
# ---------------------------------------------------------------------------


class TestDelete:
    def test_delete_returns_none(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="DELETE",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/beacon-id-001",
            status_code=204,
        )
        result = api.delete("beacon-id-001")
        assert result is None

    def test_delete_not_found_raises(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="DELETE",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/ghost",
            status_code=404,
        )
        with pytest.raises(BeaconNotFoundError):
            api.delete("ghost")


# ---------------------------------------------------------------------------
# Health
# ---------------------------------------------------------------------------


class TestHealth:
    def test_health_posts_status(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/beacons/health",
            status_code=200,
        )
        api.health(
            beacon_id="beacon-id-001",
            status="active",
            beacon_token="token-abc",
        )
        request = httpx_mock.get_request()
        import json
        body = json.loads(request.content)
        assert body["status"] == "active"
        assert body["beacon_id"] == "beacon-id-001"
        assert request.headers["X-Beacon-Token"] == "token-abc"


# ---------------------------------------------------------------------------
# Test connection
# ---------------------------------------------------------------------------


class TestTestConnection:
    def test_reachable_target(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/beacon-id-001/test",
            json={"reachable": True, "latency_ms": 12},
        )
        result = api.test_connection("beacon-id-001", "192.168.1.1")
        assert result["reachable"] is True
        assert result["latency_ms"] == 12

    def test_unreachable_target(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/organizations/test-org-id/beacons/beacon-id-001/test",
            json={"reachable": False, "detail": "Connection timed out"},
        )
        result = api.test_connection("beacon-id-001", "10.0.0.99")
        assert result["reachable"] is False


# ---------------------------------------------------------------------------
# Connection error handling
# ---------------------------------------------------------------------------


class TestConnectionErrors:
    def test_connect_error_raises_api_error(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_exception(httpx.ConnectError("Connection refused"))
        with pytest.raises(BeaconAPIError, match="Could not connect"):
            api.get("any-id")

    def test_timeout_raises_api_error(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_exception(httpx.ReadTimeout("timed out"))
        with pytest.raises(BeaconAPIError, match="timed out"):
            api.get("any-id")


# ---------------------------------------------------------------------------
# Get events
# ---------------------------------------------------------------------------


class TestGetEvents:
    def test_returns_events_list(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/events/?per_page=50",
            json={"events": [{"id": 1, "slug": "pentest-q1", "name": "Q1 Pentest"}]},
        )
        events = api.get_events()
        assert len(events) == 1
        assert events[0]["slug"] == "pentest-q1"

    def test_passes_organization_id_as_param(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/events/?per_page=50&organization_id={TEST_ORG_ID}",
            json={"events": [{"id": 2, "slug": "pentest-q2"}]},
        )
        events = api.get_events(organization_id=TEST_ORG_ID)
        assert events[0]["slug"] == "pentest-q2"

    def test_falls_back_to_results_key(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/events/?per_page=50",
            json={"results": [{"id": 3, "slug": "old-format"}]},
        )
        events = api.get_events()
        assert events[0]["slug"] == "old-format"

    def test_returns_empty_list_when_no_events(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/events/?per_page=50",
            json={"events": []},
        )
        assert api.get_events() == []

    def test_auth_error_raises(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="GET",
            url=f"{BASE_URL}/v1/events/?per_page=50",
            status_code=401,
        )
        with pytest.raises(BeaconAuthError):
            api.get_events()


# ---------------------------------------------------------------------------
# Create asset
# ---------------------------------------------------------------------------


class TestCreateAsset:
    def test_creates_cidr_asset_success(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/assets/",
            json={"id": 42, "value": "192.168.1.0/24", "type": "cidr"},
            status_code=201,
        )
        result = api.create_asset(
            organization_id="5",
            asset_value="192.168.1.0/24",
            asset_type="cidr",
            beacon_id=99,
        )
        assert result["id"] == 42

    def test_request_body_contains_correct_fields(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/assets/",
            json={"id": 1},
            status_code=201,
        )
        api.create_asset(
            organization_id="5",
            asset_value="10.0.0.0/8",
            asset_type="cidr",
            beacon_id=99,
        )
        import json
        body = json.loads(httpx_mock.get_request().content)
        assert body["organization_id"] == 5
        assert body["asset_value"] == "10.0.0.0/8"
        assert body["asset_type"] == "cidr"
        assert body["beacon_id"] == 99

    def test_conflict_raises_api_error_with_409(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/assets/",
            status_code=409,
            json={"detail": "Asset already exists"},
        )
        with pytest.raises(BeaconAPIError) as exc_info:
            api.create_asset("5", "192.168.1.0/24", "cidr", 99)
        assert exc_info.value.status_code == 409

    def test_auth_error_raises(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/assets/",
            status_code=401,
        )
        with pytest.raises(BeaconAuthError):
            api.create_asset("5", "192.168.1.0/24", "cidr", 99)


# ---------------------------------------------------------------------------
# Add asset to event
# ---------------------------------------------------------------------------


class TestAddAssetToEvent:
    def test_adds_asset_to_pentest_scope(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/events/pentest-q1/scope/assets/",
            json={"id": 100, "asset_id": 42, "in_scope": True},
            status_code=201,
        )
        result = api.add_asset_to_event(pentest_slug="pentest-q1", asset_id=42)
        assert result["in_scope"] is True

    def test_request_body_includes_asset_id_and_in_scope(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/events/pentest-q1/scope/assets/",
            json={"id": 1},
            status_code=201,
        )
        api.add_asset_to_event(pentest_slug="pentest-q1", asset_id=99)
        import json
        body = json.loads(httpx_mock.get_request().content)
        assert body["asset_id"] == 99
        assert body["in_scope"] is True

    def test_not_found_raises_not_found_error(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/events/nonexistent/scope/assets/",
            status_code=404,
        )
        with pytest.raises(BeaconNotFoundError):
            api.add_asset_to_event(pentest_slug="nonexistent", asset_id=1)

    def test_conflict_raises_api_error_with_409(self, api, httpx_mock: HTTPXMock):
        httpx_mock.add_response(
            method="POST",
            url=f"{BASE_URL}/v1/events/pentest-q1/scope/assets/",
            status_code=409,
            json={"detail": "Already in pentest scope"},
        )
        with pytest.raises(BeaconAPIError) as exc_info:
            api.add_asset_to_event(pentest_slug="pentest-q1", asset_id=1)
        assert exc_info.value.status_code == 409
