"""Tests for beta license module."""
import json
import time
from pathlib import Path
from unittest.mock import patch

import pytest

from greymatter_core.license import (
    BETA_DURATION_DAYS,
    GRACE_PERIOD_DAYS,
    LicenseStatus,
    _load_cached_license,
    _save_license,
    activate,
    check_license_or_exit,
    generate_fingerprint,
    sign_license,
    verify,
    verify_signature,
)


# --- Fingerprint ---


class TestFingerprint:
    def test_returns_64_char_hex(self):
        fp = generate_fingerprint()
        assert len(fp) == 64
        assert all(c in "0123456789abcdef" for c in fp)

    def test_deterministic(self):
        assert generate_fingerprint() == generate_fingerprint()


# --- HMAC Signing ---


class TestSigning:
    def _make_license_data(self, **overrides) -> dict:
        now = time.time()
        data = {
            "machine_id": "abc123",
            "activated_at": now,
            "expires_at": now + 86400 * 30,
        }
        data.update(overrides)
        return data

    def test_sign_returns_hex(self):
        data = self._make_license_data()
        sig = sign_license(data)
        assert len(sig) == 64
        assert all(c in "0123456789abcdef" for c in sig)

    def test_sign_deterministic(self):
        data = self._make_license_data()
        assert sign_license(data) == sign_license(data)

    def test_different_keys_produce_different_sigs(self):
        data = self._make_license_data()
        sig1 = sign_license(data, key=b"key-a")
        sig2 = sign_license(data, key=b"key-b")
        assert sig1 != sig2

    def test_verify_valid_signature(self):
        data = self._make_license_data()
        data["signature"] = sign_license(data)
        assert verify_signature(data) is True

    def test_verify_invalid_signature(self):
        data = self._make_license_data()
        data["signature"] = "0" * 64
        assert verify_signature(data) is False

    def test_verify_missing_signature(self):
        data = self._make_license_data()
        assert verify_signature(data) is False

    def test_tampered_expires_at(self):
        data = self._make_license_data()
        data["signature"] = sign_license(data)
        data["expires_at"] = data["expires_at"] + 86400 * 365  # tamper
        assert verify_signature(data) is False


# --- License Cache ---


class TestLicenseCache:
    def test_save_and_load(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        data = {"machine_id": "test", "activated_at": 1.0, "expires_at": 2.0}
        _save_license(data)

        loaded = _load_cached_license()
        assert loaded == data

    def test_load_missing_returns_none(self, tmp_path, monkeypatch):
        license_path = tmp_path / "nonexistent.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        assert _load_cached_license() is None

    def test_load_corrupt_returns_none(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        license_path.write_text("not json{{{")
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        assert _load_cached_license() is None

    def test_saved_file_permissions(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        _save_license({"test": True})
        mode = license_path.stat().st_mode & 0o777
        assert mode == 0o600


# --- Verification ---


class TestVerify:
    def _make_valid_license(self, fingerprint: str, now: float) -> dict:
        data = {
            "machine_id": fingerprint,
            "activated_at": now,
            "expires_at": now + BETA_DURATION_DAYS * 86400,
        }
        data["signature"] = sign_license(data)
        return data

    def test_valid_license(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        fp = "a" * 64
        now = time.time()
        data = self._make_valid_license(fp, now)
        _save_license(data)

        status = verify(fingerprint=fp, now=now)
        assert status.valid is True
        assert status.days_remaining == BETA_DURATION_DAYS
        assert status.grace is False

    def test_no_license_file(self, tmp_path, monkeypatch):
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", tmp_path / "nope.json")
        status = verify(fingerprint="x" * 64)
        assert status.valid is False
        assert "No license" in status.message

    def test_wrong_machine(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        fp = "a" * 64
        now = time.time()
        data = self._make_valid_license(fp, now)
        _save_license(data)

        status = verify(fingerprint="b" * 64, now=now)
        assert status.valid is False
        assert "different machine" in status.message

    def test_invalid_signature(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        fp = "a" * 64
        now = time.time()
        data = self._make_valid_license(fp, now)
        data["signature"] = "0" * 64  # tamper
        _save_license(data)

        status = verify(fingerprint=fp, now=now)
        assert status.valid is False
        assert "signature" in status.message.lower()

    def test_expired_license(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        fp = "a" * 64
        activated = time.time() - 90 * 86400  # 90 days ago
        data = self._make_valid_license(fp, activated)
        _save_license(data)

        status = verify(fingerprint=fp)
        assert status.valid is False
        assert "expired" in status.message.lower()

    def test_grace_period(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        fp = "a" * 64
        now = time.time()
        # License that expired 3 days ago (within 7-day grace)
        expires_at = now - 3 * 86400
        data = {
            "machine_id": fp,
            "activated_at": expires_at - BETA_DURATION_DAYS * 86400,
            "expires_at": expires_at,
        }
        data["signature"] = sign_license(data)
        _save_license(data)

        status = verify(fingerprint=fp, now=now)
        assert status.valid is True
        assert status.grace is True
        assert status.days_remaining > 0

    def test_grace_period_expired(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        fp = "a" * 64
        now = time.time()
        # License that expired 10 days ago (beyond 7-day grace)
        expires_at = now - 10 * 86400
        data = {
            "machine_id": fp,
            "activated_at": expires_at - BETA_DURATION_DAYS * 86400,
            "expires_at": expires_at,
        }
        data["signature"] = sign_license(data)
        _save_license(data)

        status = verify(fingerprint=fp, now=now)
        assert status.valid is False
        assert "expired" in status.message.lower()


# --- Activation ---


class TestActivation:
    def test_activate_success(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        fp = "a" * 64
        now = time.time()
        server_response = {
            "machine_id": fp,
            "activated_at": now,
            "expires_at": now + BETA_DURATION_DAYS * 86400,
        }
        server_response["signature"] = sign_license(server_response)

        mock_response = type("Response", (), {
            "status_code": 200,
            "raise_for_status": lambda self: None,
            "json": lambda self: server_response,
        })()

        with patch("greymatter_core.license.httpx.post", return_value=mock_response):
            result = activate(server_url="http://test:9000", fingerprint=fp)

        assert result["machine_id"] == fp
        assert license_path.exists()

    def test_activate_server_error(self, monkeypatch):
        import httpx as _httpx

        def mock_post(*args, **kwargs):
            raise _httpx.ConnectError("Connection refused")

        with patch("greymatter_core.license.httpx.post", side_effect=mock_post):
            with pytest.raises(_httpx.ConnectError):
                activate(server_url="http://bad:9000", fingerprint="x" * 64)

    def test_activate_error_response(self, tmp_path, monkeypatch):
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", tmp_path / "l.json")
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        mock_response = type("Response", (), {
            "status_code": 200,
            "raise_for_status": lambda self: None,
            "json": lambda self: {"error": "Machine limit exceeded"},
        })()

        with patch("greymatter_core.license.httpx.post", return_value=mock_response):
            with pytest.raises(RuntimeError, match="Machine limit exceeded"):
                activate(server_url="http://test:9000", fingerprint="x" * 64)


# --- Startup Gate ---


class TestCheckLicenseOrExit:
    def test_exits_on_no_license_and_failed_activation(self, tmp_path, monkeypatch):
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", tmp_path / "l.json")
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        with patch("greymatter_core.license.activate", side_effect=RuntimeError("fail")):
            with pytest.raises(SystemExit) as exc_info:
                check_license_or_exit()
            assert exc_info.value.code == 1

    def test_exits_on_expired_license(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        fp = generate_fingerprint()
        now = time.time()
        # Expired 60 days ago — well past grace period
        expires_at = now - 60 * 86400
        data = {
            "machine_id": fp,
            "activated_at": expires_at - 30 * 86400,
            "expires_at": expires_at,
        }
        data["signature"] = sign_license(data)
        _save_license(data)

        with pytest.raises(SystemExit) as exc_info:
            check_license_or_exit()
        assert exc_info.value.code == 1

    def test_passes_on_valid_license(self, tmp_path, monkeypatch):
        license_path = tmp_path / "license.json"
        monkeypatch.setattr("greymatter_core.license.LICENSE_PATH", license_path)
        monkeypatch.setattr("greymatter_core.license.LICENSE_DIR", tmp_path)

        fp = generate_fingerprint()
        now = time.time()
        data = {
            "machine_id": fp,
            "activated_at": now,
            "expires_at": now + BETA_DURATION_DAYS * 86400,
        }
        data["signature"] = sign_license(data)
        _save_license(data)

        # Should not raise
        check_license_or_exit()
