"""Tests for server-side license validation (licensing.py v2)."""

import json
import pytest
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import patch, MagicMock
from urllib.error import HTTPError, URLError

from stablestack.licensing import (
    validate_license_server,
    save_license_cache,
    load_license_cache,
    check_license,
    get_license_key,
    get_cached_license,
    invalidate_license_cache,
    LicenseError,
    _REVALIDATION_DAYS,
    _OFFLINE_GRACE_DAYS,
)


@pytest.fixture(autouse=True)
def reset_license_cache():
    """Reset the in-process license cache before each test."""
    invalidate_license_cache()
    yield
    invalidate_license_cache()


@pytest.fixture
def license_dir(tmp_path):
    """Override license file paths to use a temp directory."""
    d = tmp_path / ".stablestack"
    d.mkdir()
    return d


def _mock_server_response(data: dict, status: int = 200):
    """Create a mock urlopen context manager."""
    mock_resp = MagicMock()
    mock_resp.read.return_value = json.dumps(data).encode("utf-8")
    mock_resp.__enter__ = MagicMock(return_value=mock_resp)
    mock_resp.__exit__ = MagicMock(return_value=False)
    return mock_resp


class TestValidateLicenseServer:
    @patch("stablestack.licensing.urllib.request.urlopen")
    def test_valid_key(self, mock_urlopen):
        mock_urlopen.return_value = _mock_server_response({
            "valid": True,
            "customer_name": "Test User",
            "license_type": "individual",
            "expires_at": (datetime.now() + timedelta(days=365)).isoformat(),
            "max_projects": 3,
        })
        result = validate_license_server("sk_live_test123")
        assert result["valid"] is True
        assert result["customer_name"] == "Test User"

    @patch("stablestack.licensing.urllib.request.urlopen")
    def test_invalid_key_raises(self, mock_urlopen):
        mock_urlopen.return_value = _mock_server_response({
            "valid": False,
            "error": "Invalid license key",
        })
        with pytest.raises(LicenseError, match="Invalid license key"):
            validate_license_server("sk_live_bad")

    @patch("stablestack.licensing.urllib.request.urlopen")
    def test_http_error_raises(self, mock_urlopen):
        error = HTTPError(
            url="https://api.stablestack.ai/v1/licenses/validate",
            code=403,
            msg="Forbidden",
            hdrs={},
            fp=MagicMock(read=MagicMock(return_value=b'{"error":"Forbidden"}')),
        )
        mock_urlopen.side_effect = error
        with pytest.raises(LicenseError, match="validation failed"):
            validate_license_server("sk_live_bad")

    @patch("stablestack.licensing.urllib.request.urlopen")
    def test_network_error_raises(self, mock_urlopen):
        mock_urlopen.side_effect = URLError("Connection refused")
        with pytest.raises(LicenseError, match="Could not reach"):
            validate_license_server("sk_live_test")


class TestLicenseCache:
    def test_save_and_load_round_trip(self, license_dir):
        with patch(
            "stablestack.licensing._get_license_cache_path",
            return_value=license_dir / "license.json",
        ):
            save_license_cache("sk_live_abc", {
                "customer_name": "Alice",
                "customer_email": "alice@example.com",
                "license_type": "team",
                "expires_at": "2027-01-01T00:00:00",
                "max_projects": 10,
            })
            cache = load_license_cache()
            assert cache is not None
            assert cache["key"] == "sk_live_abc"
            assert cache["customer_name"] == "Alice"
            assert cache["license_type"] == "team"

    def test_load_missing_returns_none(self, license_dir):
        with patch(
            "stablestack.licensing._get_license_cache_path",
            return_value=license_dir / "nonexistent.json",
        ):
            assert load_license_cache() is None


class TestCheckLicense:
    def test_no_key_returns_false(self):
        with patch("stablestack.licensing.load_license_cache", return_value=None), \
             patch("stablestack.licensing.get_license_key", return_value=None):
            valid, info, error = check_license()
            assert valid is False
            assert info is None
            assert "No license key" in error

    def test_cached_valid_license(self):
        cache = {
            "key": "sk_live_test",
            "customer_name": "Bob",
            "license_type": "individual",
            "expires_at": (datetime.now() + timedelta(days=30)).isoformat(),
            "max_projects": 3,
            "validated_at": datetime.now().isoformat(),
        }
        with patch("stablestack.licensing.load_license_cache", return_value=cache):
            valid, info, error = check_license()
            assert valid is True
            assert info is not None
            assert info.customer_name == "Bob"
            assert error is None

    def test_expired_license_returns_false(self):
        cache = {
            "key": "sk_live_test",
            "customer_name": "Bob",
            "license_type": "individual",
            "expires_at": (datetime.now() - timedelta(days=1)).isoformat(),
            "max_projects": 3,
            "validated_at": datetime.now().isoformat(),
        }
        with patch("stablestack.licensing.load_license_cache", return_value=cache):
            valid, info, error = check_license()
            assert valid is False
            assert "expired" in error.lower()

    @patch("stablestack.licensing.validate_license_server")
    def test_revalidation_on_stale_cache(self, mock_validate):
        stale_date = (datetime.now() - timedelta(days=_REVALIDATION_DAYS + 1)).isoformat()
        cache = {
            "key": "sk_live_test",
            "customer_name": "Bob",
            "license_type": "individual",
            "expires_at": (datetime.now() + timedelta(days=30)).isoformat(),
            "max_projects": 3,
            "validated_at": stale_date,
        }
        mock_validate.return_value = {
            "valid": True,
            "customer_name": "Bob",
            "license_type": "individual",
            "expires_at": (datetime.now() + timedelta(days=30)).isoformat(),
            "max_projects": 3,
        }
        with patch("stablestack.licensing.load_license_cache", return_value=cache), \
             patch("stablestack.licensing.save_license_cache"):
            valid, info, error = check_license()
            assert valid is True
            mock_validate.assert_called_once()

    @patch("stablestack.licensing.validate_license_server")
    def test_offline_grace_period(self, mock_validate):
        """Server unreachable within grace period should still allow use."""
        stale_date = (
            datetime.now() - timedelta(days=_REVALIDATION_DAYS + 1)
        ).isoformat()
        cache = {
            "key": "sk_live_test",
            "customer_name": "Bob",
            "license_type": "individual",
            "expires_at": (datetime.now() + timedelta(days=30)).isoformat(),
            "max_projects": 3,
            "validated_at": stale_date,
        }
        mock_validate.side_effect = LicenseError("Network error")
        with patch("stablestack.licensing.load_license_cache", return_value=cache):
            valid, info, error = check_license()
            assert valid is True  # grace period still active

    @patch("stablestack.licensing.validate_license_server")
    def test_grace_period_expired(self, mock_validate):
        """Server unreachable past grace period should fail."""
        stale_date = (
            datetime.now() - timedelta(days=_OFFLINE_GRACE_DAYS + 1)
        ).isoformat()
        cache = {
            "key": "sk_live_test",
            "customer_name": "Bob",
            "license_type": "individual",
            "expires_at": (datetime.now() + timedelta(days=30)).isoformat(),
            "max_projects": 3,
            "validated_at": stale_date,
        }
        mock_validate.side_effect = LicenseError("Network error")
        with patch("stablestack.licensing.load_license_cache", return_value=cache):
            valid, info, error = check_license()
            assert valid is False
            assert "grace period" in error.lower()


class TestGetLicenseKey:
    def test_env_var_takes_precedence(self):
        with patch.dict("os.environ", {"STABLESTACK_LICENSE": "sk_from_env"}):
            assert get_license_key() == "sk_from_env"

    def test_cached_key_from_file(self):
        with patch.dict("os.environ", {}, clear=True), \
             patch("stablestack.licensing.load_license_cache", return_value={"key": "sk_cached"}):
            assert get_license_key() == "sk_cached"

    def test_no_key_returns_none(self):
        with patch.dict("os.environ", {}, clear=True), \
             patch("stablestack.licensing.load_license_cache", return_value=None), \
             patch("pathlib.Path.exists", return_value=False):
            assert get_license_key() is None
