"""Tests for LicensesOSClient -- all 6 API methods, static helpers, and error handling."""

from __future__ import annotations

import json
import time

import httpx
import pytest
from pytest_httpx import HTTPXMock

from licensesos.client import LicensesOSClient
from licensesos.errors import ApiError, NetworkError
from licensesos.results.activation import ActivationResult
from licensesos.results.activations import ActivationsResult
from licensesos.results.deactivation import DeactivationResult
from licensesos.results.heartbeat import HeartbeatResult
from licensesos.results.usage import UsageResult
from licensesos.results.validation import ValidationResult

API_KEY = "test_api_key_123"
BASE_URL = "https://api.test.com"


# ---------------------------------------------------------------------------
# Constructor
# ---------------------------------------------------------------------------


class TestConstructor:
    def test_raises_if_api_key_empty(self) -> None:
        with pytest.raises(ValueError, match="api_key is required"):
            LicensesOSClient("")

    def test_strips_trailing_slashes(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/validate",
            json={"success": True, "data": {"valid": True}},
        )
        client = LicensesOSClient(API_KEY, base_url=f"{BASE_URL}///")
        result = client.validate("key")
        assert result.is_valid() is True
        client.close()

    def test_context_manager(self) -> None:
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            assert client is not None


# ---------------------------------------------------------------------------
# Static helpers
# ---------------------------------------------------------------------------


class TestNormalizeDomain:
    @pytest.mark.parametrize(
        "input_str,expected",
        [
            ("https://www.Example.Com:8080/path?q=1", "example.com"),
            ("http://WWW.test.org/", "test.org"),
            ("example.com", "example.com"),
            ("HTTPS://APP.EXAMPLE.IO", "app.example.io"),
            ("  example.com  ", "example.com"),
            ("www.example.com", "example.com"),
            ("localhost:3000", "localhost"),
            ("http://sub.domain.com:443/foo/bar", "sub.domain.com"),
        ],
    )
    def test_normalizes(self, input_str: str, expected: str) -> None:
        assert LicensesOSClient.normalize_domain(input_str) == expected


class TestShouldAllowPremium:
    def test_returns_true_for_active_within_grace(self) -> None:
        cached_at = int(time.time()) - 100
        assert LicensesOSClient.should_allow_premium({"status": "active", "cached_at": cached_at}) is True

    def test_returns_false_for_non_active_status(self) -> None:
        cached_at = int(time.time())
        assert LicensesOSClient.should_allow_premium({"status": "expired", "cached_at": cached_at}) is False
        assert LicensesOSClient.should_allow_premium({"status": "revoked", "cached_at": cached_at}) is False
        assert LicensesOSClient.should_allow_premium({"cached_at": cached_at}) is False

    def test_returns_false_when_cache_too_old(self) -> None:
        # 12h cache + 48h grace = 60h = 216000 seconds
        cached_at = int(time.time()) - 300_000  # ~83 hours ago
        assert LicensesOSClient.should_allow_premium({"status": "active", "cached_at": cached_at}) is False

    def test_returns_true_at_boundary(self) -> None:
        # Exactly at 12h + 48h = 216000 seconds
        cached_at = int(time.time()) - 216_000
        assert LicensesOSClient.should_allow_premium({"status": "active", "cached_at": cached_at}) is True

    def test_accepts_custom_grace_ttl(self) -> None:
        # 12h cache + 0 grace = 43200 seconds
        cached_at = int(time.time()) - 50_000  # >12h ago
        assert LicensesOSClient.should_allow_premium({"status": "active", "cached_at": cached_at}, 0) is False


# ---------------------------------------------------------------------------
# API methods
# ---------------------------------------------------------------------------


class TestValidate:
    def test_sends_post_and_returns_validation_result(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/validate",
            json={
                "success": True,
                "data": {
                    "valid": True,
                    "license": {
                        "id": "lic_1",
                        "status": "active",
                        "entitlements": None,
                        "expires_at": None,
                        "is_expired": False,
                        "activation_limit": None,
                        "activation_count": 0,
                        "uses_count": 0,
                        "max_uses": None,
                        "uses_remaining": None,
                        "usage_reset_interval": None,
                        "uses_reset_at": None,
                    },
                },
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            result = client.validate("LIC-KEY-123")

        assert isinstance(result, ValidationResult)
        assert result.is_valid() is True

        request = httpx_mock.get_requests()[0]
        assert request.method == "POST"
        assert "Bearer test_api_key_123" in request.headers["authorization"]
        body = json.loads(request.content)
        assert body["license_key"] == "LIC-KEY-123"

    def test_normalizes_identifier(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/validate",
            json={
                "success": True,
                "data": {"valid": True, "license": {"id": "x", "status": "active"}},
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            client.validate("KEY", "https://WWW.Example.Com/path")

        body = json.loads(httpx_mock.get_requests()[0].content)
        assert body["identifier"] == "example.com"

    def test_includes_metadata(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/validate",
            json={"success": True, "data": {"valid": True}},
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            client.validate("KEY", metadata={"version": "2.0"})

        body = json.loads(httpx_mock.get_requests()[0].content)
        assert body["metadata"] == {"version": "2.0"}
        assert "identifier" not in body

    def test_returns_result_for_invalid_license(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/validate",
            json={
                "success": True,
                "data": {
                    "valid": False,
                    "error": {"code": "LICENSE_NOT_FOUND", "message": "License not found"},
                },
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            result = client.validate("BAD-KEY")

        assert isinstance(result, ValidationResult)
        assert result.is_valid() is False
        assert result.error is not None
        assert result.error["code"] == "LICENSE_NOT_FOUND"


class TestActivate:
    def test_sends_activation_and_returns_result(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/activate",
            json={
                "success": True,
                "data": {
                    "activation": {
                        "id": "act_1",
                        "identifier": "example.com",
                        "label": None,
                        "status": "active",
                        "activated_at": "2025-01-01T00:00:00+00:00",
                        "last_seen_at": None,
                        "metadata": None,
                    },
                    "license": {
                        "id": "lic_1",
                        "status": "active",
                        "entitlements": None,
                        "expires_at": None,
                        "activation_count": 1,
                        "activation_limit": 5,
                    },
                },
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            result = client.activate(
                "KEY",
                "https://example.com",
                label="Production",
                metadata={"env": "prod"},
            )

        assert isinstance(result, ActivationResult)
        assert result.is_activated() is True
        assert result.get_remaining_activations() == 4

        body = json.loads(httpx_mock.get_requests()[0].content)
        assert body["identifier"] == "example.com"
        assert body["label"] == "Production"
        assert body["metadata"] == {"env": "prod"}


class TestDeactivate:
    def test_sends_deactivation_and_returns_result(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/deactivate",
            json={"success": True, "data": {"deactivated": True}},
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            result = client.deactivate("KEY", "example.com")

        assert isinstance(result, DeactivationResult)
        assert result.is_deactivated() is True

    def test_does_not_normalize_identifier(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/deactivate",
            json={"success": True, "data": {"deactivated": True}},
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            client.deactivate("KEY", "https://WWW.Example.Com/path")

        body = json.loads(httpx_mock.get_requests()[0].content)
        # Deactivate does NOT normalize (per plan spec)
        assert body["identifier"] == "https://WWW.Example.Com/path"


class TestListActivations:
    def test_sends_get_and_returns_result(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/KEY-123/activations",
            json={
                "success": True,
                "data": {
                    "activations": [
                        {
                            "id": "act_1",
                            "identifier": "device-a",
                            "label": None,
                            "status": "active",
                            "activated_at": "2025-01-01T00:00:00+00:00",
                            "last_seen_at": None,
                            "metadata": None,
                        },
                    ],
                    "count": 1,
                    "limit": 5,
                },
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            result = client.list_activations("KEY-123")

        assert isinstance(result, ActivationsResult)
        assert result.count == 1

        request = httpx_mock.get_requests()[0]
        assert request.method == "GET"
        assert "/api/v1/licenses/KEY-123/activations" in str(request.url)


class TestHeartbeat:
    def test_sends_heartbeat_and_returns_result(self, httpx_mock: HTTPXMock) -> None:
        from datetime import datetime, timedelta, timezone

        next_check_in = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/heartbeat",
            json={
                "success": True,
                "data": {
                    "activation": {
                        "id": "act_1",
                        "identifier": "device-abc",
                        "label": None,
                        "status": "active",
                        "activated_at": "2025-01-01T00:00:00+00:00",
                        "last_seen_at": datetime.now(timezone.utc).isoformat(),
                        "metadata": None,
                    },
                    "next_check_in_at": next_check_in,
                },
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            result = client.heartbeat("KEY", "device-abc")

        assert isinstance(result, HeartbeatResult)
        assert result.next_check_in_at == next_check_in

    def test_does_not_normalize_identifier(self, httpx_mock: HTTPXMock) -> None:
        from datetime import datetime, timezone

        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/heartbeat",
            json={
                "success": True,
                "data": {
                    "activation": {
                        "id": "act_1",
                        "identifier": "device-abc",
                        "label": None,
                        "status": "active",
                        "activated_at": "2025-01-01T00:00:00+00:00",
                        "last_seen_at": None,
                        "metadata": None,
                    },
                    "next_check_in_at": datetime.now(timezone.utc).isoformat(),
                },
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            client.heartbeat("KEY", "My-Device-ABC")

        body = json.loads(httpx_mock.get_requests()[0].content)
        # Heartbeat does NOT normalize
        assert body["identifier"] == "My-Device-ABC"


class TestIncrementUsage:
    def test_sends_usage_increment_and_returns_result(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/usage/increment",
            json={
                "success": True,
                "data": {
                    "usage": {
                        "uses_count": 11,
                        "max_uses": 100,
                        "uses_remaining": 89,
                        "usage_reset_interval": "monthly",
                        "uses_reset_at": "2025-02-01T00:00:00+00:00",
                    },
                },
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            result = client.increment_usage("KEY", 5)

        assert isinstance(result, UsageResult)
        assert result.usage["uses_count"] == 11

        body = json.loads(httpx_mock.get_requests()[0].content)
        assert body["amount"] == 5

    def test_omits_amount_when_default(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/usage/increment",
            json={
                "success": True,
                "data": {
                    "usage": {
                        "uses_count": 1,
                        "max_uses": None,
                        "uses_remaining": None,
                        "usage_reset_interval": None,
                        "uses_reset_at": None,
                    },
                },
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            client.increment_usage("KEY")

        body = json.loads(httpx_mock.get_requests()[0].content)
        assert "amount" not in body


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------


class TestErrorHandling:
    def test_raises_api_error_for_non_2xx(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/activate",
            status_code=404,
            json={
                "success": False,
                "error": {"code": "LICENSE_NOT_FOUND", "message": "License not found"},
            },
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            with pytest.raises(ApiError) as exc_info:
                client.activate("KEY", "id")

        err = exc_info.value
        assert err.code == "LICENSE_NOT_FOUND"
        assert err.status_code == 404
        assert str(err) == "License not found"

    def test_raises_api_error_with_unknown_for_no_error_body(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_response(
            url=f"{BASE_URL}/api/v1/licenses/validate",
            status_code=500,
            json={"success": False},
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            with pytest.raises(ApiError) as exc_info:
                client.validate("KEY")

        err = exc_info.value
        assert err.code == "UNKNOWN_ERROR"
        assert err.status_code == 500

    def test_raises_network_error_on_connection_failure(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_exception(
            httpx.ConnectError("Connection refused"),
            url=f"{BASE_URL}/api/v1/licenses/validate",
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            with pytest.raises(NetworkError) as exc_info:
                client.validate("KEY")

        err = exc_info.value
        assert "Failed to connect" in str(err)
        assert err.cause is not None

    def test_raises_network_error_on_timeout(self, httpx_mock: HTTPXMock) -> None:
        httpx_mock.add_exception(
            httpx.ReadTimeout("Read timed out"),
            url=f"{BASE_URL}/api/v1/licenses/validate",
        )
        with LicensesOSClient(API_KEY, base_url=BASE_URL) as client:
            with pytest.raises(NetworkError) as exc_info:
                client.validate("KEY")

        err = exc_info.value
        assert "timed out" in str(err)
