"""Tests for src/cli/license.py."""

import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

import src.cli.license as lic


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def isolated_license_file(tmp_path, monkeypatch):
    """Redirect all license file I/O to a temp directory."""
    monkeypatch.setattr(lic, "CONFIG_DIR", tmp_path)
    monkeypatch.setattr(lic, "LICENSE_FILE", tmp_path / "license.json")


def _write_license(tmp_path, data: dict):
    (tmp_path / "license.json").write_text(json.dumps(data))


def _read_license(tmp_path) -> dict:
    return json.loads((tmp_path / "license.json").read_text())


def _days_ago(n: int) -> str:
    return (datetime.now(timezone.utc) - timedelta(days=n)).isoformat()


def _days_from_now(n: int) -> str:
    return (datetime.now(timezone.utc) + timedelta(days=n)).isoformat()


# ---------------------------------------------------------------------------
# Trial
# ---------------------------------------------------------------------------

def test_trial_starts_on_first_run(tmp_path):
    allowed, msg = lic.check()
    assert allowed
    assert "Trial" in msg
    data = _read_license(tmp_path)
    assert "trial_started" in data


def test_trial_shows_days_remaining(tmp_path):
    _write_license(tmp_path, {"trial_started": _days_ago(5)})
    allowed, msg = lic.check()
    assert allowed
    assert "25" in msg  # 30 - 5 = 25 days left


def test_trial_last_day_still_allowed(tmp_path):
    _write_license(tmp_path, {"trial_started": _days_ago(29)})
    allowed, msg = lic.check()
    assert allowed
    assert "1 day" in msg


def test_trial_expired_blocks(tmp_path):
    _write_license(tmp_path, {"trial_started": _days_ago(30)})
    allowed, msg = lic.check()
    assert not allowed
    assert "expired" in msg.lower()


def test_trial_expired_shows_purchase_url(tmp_path):
    _write_license(tmp_path, {"trial_started": _days_ago(31)})
    allowed, msg = lic.check()
    assert not allowed
    assert lic.PURCHASE_URL in msg


# ---------------------------------------------------------------------------
# Activate
# ---------------------------------------------------------------------------

def _mock_activate_response(email="user@example.com", instance_id="inst-123"):
    body = {
        "activated": True,
        "instance": {"id": instance_id},
        "meta": {"customer_email": email},
    }
    mock = MagicMock()
    mock.json.return_value = body
    return mock


def test_activate_success_stores_license(tmp_path):
    with patch("requests.post", return_value=_mock_activate_response()):
        lic.activate("TEST-KEY-123")
    data = _read_license(tmp_path)
    assert data["key"] == "TEST-KEY-123"
    assert data["instance_id"] == "inst-123"
    assert data["email"] == "user@example.com"
    assert data["valid"] is True


def test_activate_success_returns_body():
    with patch("requests.post", return_value=_mock_activate_response()):
        body = lic.activate("TEST-KEY-123")
    assert body["activated"] is True


def test_activate_preserves_trial_started(tmp_path):
    _write_license(tmp_path, {"trial_started": _days_ago(10)})
    with patch("requests.post", return_value=_mock_activate_response()):
        lic.activate("TEST-KEY-123")
    data = _read_license(tmp_path)
    assert "trial_started" in data


def test_activate_network_error_raises():
    import requests as req
    with patch("requests.post", side_effect=req.RequestException("timeout")):
        with pytest.raises(RuntimeError, match="Could not reach activation server"):
            lic.activate("TEST-KEY-123")


def test_activate_invalid_key_raises():
    mock = MagicMock()
    mock.json.return_value = {"activated": False, "error": "Invalid license key"}
    with patch("requests.post", return_value=mock):
        with pytest.raises(RuntimeError, match="Activation failed"):
            lic.activate("BAD-KEY")


def test_activate_machine_limit_raises():
    mock = MagicMock()
    mock.json.return_value = {
        "activated": False,
        "error": "You have exceeded your activation limit",
    }
    with patch("requests.post", return_value=mock):
        with pytest.raises(RuntimeError, match="Machine limit reached"):
            lic.activate("MAXED-KEY")


# ---------------------------------------------------------------------------
# Deactivate
# ---------------------------------------------------------------------------

def test_deactivate_success_clears_license(tmp_path):
    _write_license(tmp_path, {
        "key": "TEST-KEY",
        "instance_id": "inst-123",
        "valid": True,
    })
    mock = MagicMock()
    mock.json.return_value = {"deactivated": True}
    with patch("requests.post", return_value=mock):
        lic.deactivate()
    data = _read_license(tmp_path)
    assert "key" not in data
    assert "instance_id" not in data


def test_deactivate_preserves_trial_started(tmp_path):
    _write_license(tmp_path, {
        "key": "TEST-KEY",
        "instance_id": "inst-123",
        "trial_started": _days_ago(10),
        "valid": True,
    })
    mock = MagicMock()
    mock.json.return_value = {"deactivated": True}
    with patch("requests.post", return_value=mock):
        lic.deactivate()
    data = _read_license(tmp_path)
    assert "trial_started" in data


def test_deactivate_no_license_raises(tmp_path):
    with pytest.raises(RuntimeError, match="No active license"):
        lic.deactivate()


def test_deactivate_network_error_raises(tmp_path):
    import requests as req
    _write_license(tmp_path, {"key": "TEST-KEY", "instance_id": "inst-123"})
    with patch("requests.post", side_effect=req.RequestException("timeout")):
        with pytest.raises(RuntimeError, match="Could not reach activation server"):
            lic.deactivate()


def test_deactivate_api_failure_raises(tmp_path):
    _write_license(tmp_path, {"key": "TEST-KEY", "instance_id": "inst-123"})
    mock = MagicMock()
    mock.json.return_value = {"deactivated": False, "error": "Instance not found"}
    with patch("requests.post", return_value=mock):
        with pytest.raises(RuntimeError, match="Deactivation failed"):
            lic.deactivate()


# ---------------------------------------------------------------------------
# check() — licensed
# ---------------------------------------------------------------------------

def test_check_valid_license_fresh_cache(tmp_path):
    """Cache is recent — no API call, should pass silently."""
    _write_license(tmp_path, {
        "key": "TEST-KEY",
        "instance_id": "inst-123",
        "valid": True,
        "last_validated": _days_ago(1),
    })
    allowed, msg = lic.check()
    assert allowed
    assert msg == ""


def test_check_valid_license_stale_revalidates_online(tmp_path):
    """Cache is stale — should call API and pass if valid."""
    _write_license(tmp_path, {
        "key": "TEST-KEY",
        "instance_id": "inst-123",
        "valid": True,
        "last_validated": _days_ago(8),
    })
    mock = MagicMock()
    mock.json.return_value = {"valid": True}
    with patch("requests.post", return_value=mock):
        allowed, msg = lic.check()
    assert allowed
    assert msg == ""


def test_check_stale_license_revoked_online(tmp_path):
    """Stale cache + API says invalid → blocked."""
    _write_license(tmp_path, {
        "key": "TEST-KEY",
        "instance_id": "inst-123",
        "valid": True,
        "last_validated": _days_ago(8),
    })
    mock = MagicMock()
    mock.json.return_value = {"valid": False}
    with patch("requests.post", return_value=mock):
        allowed, msg = lic.check()
    assert not allowed
    assert "License check failed" in msg


def test_check_stale_offline_within_grace(tmp_path):
    """Stale cache + offline + expiry far away → allowed (>45 days to expiry)."""
    import requests as req
    from datetime import datetime, timedelta, timezone
    far_expiry = (datetime.now(timezone.utc) + timedelta(days=200)).isoformat()
    _write_license(tmp_path, {
        "key": "TEST-KEY",
        "instance_id": "inst-123",
        "valid": True,
        "last_validated": _days_ago(10),
        "expires_at": far_expiry,
    })
    with patch("requests.post", side_effect=req.RequestException("offline")):
        allowed, msg = lic.check()
    assert allowed


def test_check_stale_offline_beyond_grace(tmp_path):
    """Stale cache + offline + expiry within 45-day window → blocked."""
    import requests as req
    from datetime import datetime, timedelta, timezone
    near_expiry = (datetime.now(timezone.utc) + timedelta(days=30)).isoformat()
    _write_license(tmp_path, {
        "key": "TEST-KEY",
        "instance_id": "inst-123",
        "valid": True,
        "last_validated": _days_ago(10),
        "expires_at": near_expiry,
    })
    with patch("requests.post", side_effect=req.RequestException("offline")):
        allowed, msg = lic.check()
    assert not allowed


def test_check_offline_no_expiry_date_allows(tmp_path):
    """No expires_at stored + offline → allowed (can't determine proximity)."""
    import requests as req
    _write_license(tmp_path, {
        "key": "TEST-KEY",
        "instance_id": "inst-123",
        "valid": True,
        "last_validated": _days_ago(10),
    })
    with patch("requests.post", side_effect=req.RequestException("offline")):
        allowed, _ = lic.check()
    assert allowed
