#!/usr/bin/env python3
"""
Tests for MEDUSA licensing module (medusa/core/licensing.py)

Priority 1: CRITICAL - Licensing controls access to paid features
"""

import pytest
import os
import json
import base64
from pathlib import Path
from datetime import datetime, timedelta
from unittest.mock import patch, MagicMock

from medusa.core.licensing import (
    LicenseTier,
    LicenseInfo,
    LicenseManager,
    TIER_FEATURES,
    get_license_manager,
    has_feature,
    can_use_runtime_filters,
    get_license
)


class TestLicenseTier:
    """Test LicenseTier enum"""

    def test_license_tier_values(self):
        """Test that all expected license tiers exist"""
        assert LicenseTier.FREE.value == "free"
        assert LicenseTier.PROFESSIONAL.value == "professional"
        assert LicenseTier.ENTERPRISE.value == "enterprise"

    def test_license_tier_from_string(self):
        """Test creating LicenseTier from string"""
        assert LicenseTier("free") == LicenseTier.FREE
        assert LicenseTier("professional") == LicenseTier.PROFESSIONAL
        assert LicenseTier("enterprise") == LicenseTier.ENTERPRISE


class TestLicenseInfo:
    """Test LicenseInfo dataclass"""

    def test_free_license_is_valid(self, free_license):
        """Free license should always be valid"""
        assert free_license.is_valid is True

    def test_free_license_not_paid(self, free_license):
        """Free license should not be paid"""
        assert free_license.is_paid is False

    def test_pro_license_is_paid(self, pro_license):
        """Professional license should be paid"""
        assert pro_license.is_paid is True

    def test_enterprise_license_is_paid(self, enterprise_license):
        """Enterprise license should be paid"""
        assert enterprise_license.is_paid is True

    def test_valid_pro_license(self, pro_license):
        """Valid professional license should be valid"""
        assert pro_license.is_valid is True

    def test_expired_license_invalid(self, expired_pro_license):
        """Expired license should be invalid"""
        assert expired_pro_license.is_valid is False

    def test_license_without_expiration(self):
        """License without expiration date should be invalid (except FREE)"""
        license_info = LicenseInfo(
            tier=LicenseTier.PROFESSIONAL,
            expires_at=None
        )
        assert license_info.is_valid is False

    def test_free_license_features(self, free_license):
        """Free license should have correct default values"""
        assert free_license.tier == LicenseTier.FREE
        assert free_license.max_repos == 1
        assert free_license.api_access is False
        assert free_license.runtime_filters is False
        assert free_license.custom_rules is False

    def test_pro_license_features(self, pro_license):
        """Pro license should have correct feature flags"""
        assert pro_license.tier == LicenseTier.PROFESSIONAL
        assert pro_license.max_repos == 10
        assert pro_license.api_access is True
        assert pro_license.runtime_filters is True
        assert pro_license.custom_rules is False

    def test_enterprise_license_features(self, enterprise_license):
        """Enterprise license should have all features"""
        assert enterprise_license.tier == LicenseTier.ENTERPRISE
        assert enterprise_license.max_repos == -1  # Unlimited
        assert enterprise_license.api_access is True
        assert enterprise_license.runtime_filters is True
        assert enterprise_license.custom_rules is True


class TestTierFeatures:
    """Test TIER_FEATURES dictionary"""

    def test_all_tiers_defined(self):
        """All license tiers should have feature definitions"""
        assert LicenseTier.FREE in TIER_FEATURES
        assert LicenseTier.PROFESSIONAL in TIER_FEATURES
        assert LicenseTier.ENTERPRISE in TIER_FEATURES

    def test_free_tier_features(self):
        """Free tier should have basic features only"""
        features = TIER_FEATURES[LicenseTier.FREE]
        assert features['basic_scanners'] is True
        assert features['sarif_output'] is True
        assert features['cli_access'] is True
        # Paid features disabled
        assert features['runtime_filters'] is False
        assert features['api_access'] is False
        assert features['webhooks'] is False
        assert features['custom_rules'] is False
        assert features['max_repos'] == 1

    def test_professional_tier_features(self):
        """Professional tier should have runtime filters and API"""
        features = TIER_FEATURES[LicenseTier.PROFESSIONAL]
        assert features['basic_scanners'] is True
        assert features['runtime_filters'] is True
        assert features['api_access'] is True
        assert features['webhooks'] is True
        assert features['priority_support'] is True
        # Enterprise-only features still disabled
        assert features['custom_rules'] is False
        assert features['sso'] is False
        assert features['max_repos'] == 10

    def test_enterprise_tier_features(self):
        """Enterprise tier should have all features"""
        features = TIER_FEATURES[LicenseTier.ENTERPRISE]
        assert features['basic_scanners'] is True
        assert features['runtime_filters'] is True
        assert features['api_access'] is True
        assert features['webhooks'] is True
        assert features['custom_rules'] is True
        assert features['sso'] is True
        assert features['audit_logs'] is True
        assert features['max_repos'] == -1  # Unlimited


class TestLicenseManager:
    """Test LicenseManager class"""

    def test_no_license_returns_free(self):
        """When no license is found, should return FREE tier"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            # Prevent looking at real filesystem
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', Path('/nonexistent/license.json')):
                with patch.object(manager, 'PROJECT_LICENSE_PATH', Path('/nonexistent/license.json')):
                    license_info = manager.get_license()
                    assert license_info.tier == LicenseTier.FREE
                    assert license_info.is_valid is True

    def test_environment_variable_license(self, tmp_path):
        """License from environment variable should be loaded"""
        # Create a valid license key
        license_data = {
            'tier': 'professional',
            'email': 'env@example.com',
            'exp': (datetime.now() + timedelta(days=365)).isoformat(),
        }
        # Compute signature
        import hashlib
        content = f"{license_data['tier']}:{license_data['email']}:{license_data['exp']}"
        signature = hashlib.sha256(content.encode()).hexdigest()[:16]
        license_data['sig'] = signature

        license_key = base64.b64encode(json.dumps(license_data).encode()).decode()

        with patch.dict(os.environ, {'MEDUSA_LICENSE_KEY': license_key}):
            manager = LicenseManager()
            license_info = manager.get_license()
            assert license_info.tier == LicenseTier.PROFESSIONAL
            assert license_info.email == 'env@example.com'

    def test_file_based_license(self, temp_license_file):
        """License from file should be loaded"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', temp_license_file):
                license_info = manager.get_license()
                assert license_info.tier == LicenseTier.PROFESSIONAL
                assert license_info.email == "test@example.com"
                assert license_info.organization == "Test Org"

    def test_free_license_from_file(self, temp_free_license_file):
        """FREE tier license from file should work"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', temp_free_license_file):
                license_info = manager.get_license()
                assert license_info.tier == LicenseTier.FREE

    def test_has_feature_free_tier(self):
        """Free tier should not have paid features"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', Path('/nonexistent')):
                with patch.object(manager, 'PROJECT_LICENSE_PATH', Path('/nonexistent')):
                    assert manager.has_feature('basic_scanners') is True
                    assert manager.has_feature('runtime_filters') is False
                    assert manager.has_feature('api_access') is False
                    assert manager.has_feature('custom_rules') is False

    def test_has_feature_pro_tier(self, temp_license_file):
        """Pro tier should have runtime filters"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', temp_license_file):
                assert manager.has_feature('basic_scanners') is True
                assert manager.has_feature('runtime_filters') is True
                assert manager.has_feature('api_access') is True
                assert manager.has_feature('custom_rules') is False

    def test_can_use_runtime_filters_free(self):
        """Free tier cannot use runtime filters"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', Path('/nonexistent')):
                with patch.object(manager, 'PROJECT_LICENSE_PATH', Path('/nonexistent')):
                    assert manager.can_use_runtime_filters() is False

    def test_can_use_runtime_filters_pro(self, temp_license_file):
        """Pro tier can use runtime filters"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', temp_license_file):
                assert manager.can_use_runtime_filters() is True

    def test_get_tier_name(self, temp_license_file):
        """Should return human-readable tier name"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', temp_license_file):
                assert manager.get_tier_name() == "Professional"

    def test_get_feature_list(self, temp_license_file):
        """Should return all features for tier"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', temp_license_file):
                features = manager.get_feature_list()
                assert isinstance(features, dict)
                assert 'basic_scanners' in features
                assert 'runtime_filters' in features
                assert features['runtime_filters'] is True

    def test_license_caching(self):
        """License should be cached after first load"""
        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', Path('/nonexistent')):
                with patch.object(manager, 'PROJECT_LICENSE_PATH', Path('/nonexistent')):
                    license1 = manager.get_license()
                    license2 = manager.get_license()
                    assert license1 is license2  # Same object (cached)

    def test_invalid_license_key_returns_none(self):
        """Invalid license key should return None"""
        manager = LicenseManager()
        invalid_key = "not-valid-base64-!@#$"
        result = manager._validate_license_key(invalid_key)
        assert result is None

    def test_license_key_without_signature_fails(self):
        """License key without valid signature should fail"""
        manager = LicenseManager()
        license_data = {
            'tier': 'professional',
            'email': 'test@example.com',
            'sig': 'invalid-signature'
        }
        license_key = base64.b64encode(json.dumps(license_data).encode()).decode()
        result = manager._validate_license_key(license_key)
        assert result is None


class TestConvenienceFunctions:
    """Test module-level convenience functions"""

    def test_get_license_manager_singleton(self):
        """get_license_manager should return singleton"""
        manager1 = get_license_manager()
        manager2 = get_license_manager()
        assert manager1 is manager2

    def test_has_feature_convenience(self):
        """has_feature convenience function should work"""
        with patch.dict(os.environ, {}, clear=True):
            # Reset singleton
            import medusa.core.licensing
            medusa.core.licensing._manager_instance = None

            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', Path('/nonexistent')):
                with patch.object(manager, 'PROJECT_LICENSE_PATH', Path('/nonexistent')):
                    with patch('medusa.core.licensing.get_license_manager', return_value=manager):
                        assert has_feature('basic_scanners') is True
                        assert has_feature('runtime_filters') is False

    def test_can_use_runtime_filters_convenience(self):
        """can_use_runtime_filters convenience function should work"""
        with patch.dict(os.environ, {}, clear=True):
            import medusa.core.licensing
            medusa.core.licensing._manager_instance = None

            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', Path('/nonexistent')):
                with patch.object(manager, 'PROJECT_LICENSE_PATH', Path('/nonexistent')):
                    with patch('medusa.core.licensing.get_license_manager', return_value=manager):
                        assert can_use_runtime_filters() is False

    def test_get_license_convenience(self):
        """get_license convenience function should work"""
        with patch.dict(os.environ, {}, clear=True):
            import medusa.core.licensing
            medusa.core.licensing._manager_instance = None

            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', Path('/nonexistent')):
                with patch.object(manager, 'PROJECT_LICENSE_PATH', Path('/nonexistent')):
                    with patch('medusa.core.licensing.get_license_manager', return_value=manager):
                        license_info = get_license()
                        assert license_info.tier == LicenseTier.FREE


class TestLicenseFilePaths:
    """Test license file path precedence"""

    def test_env_var_takes_precedence(self, temp_license_file):
        """Environment variable should take precedence over file"""
        # Create environment variable license
        env_license_data = {
            'tier': 'enterprise',
            'email': 'env@example.com',
            'exp': (datetime.now() + timedelta(days=365)).isoformat(),
        }
        import hashlib
        content = f"{env_license_data['tier']}:{env_license_data['email']}:{env_license_data['exp']}"
        signature = hashlib.sha256(content.encode()).hexdigest()[:16]
        env_license_data['sig'] = signature
        env_key = base64.b64encode(json.dumps(env_license_data).encode()).decode()

        with patch.dict(os.environ, {'MEDUSA_LICENSE_KEY': env_key}):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', temp_license_file):
                license_info = manager.get_license()
                # Should use env var (enterprise), not file (professional)
                assert license_info.tier == LicenseTier.ENTERPRISE
                assert license_info.email == 'env@example.com'

    def test_global_license_path_before_project(self, temp_license_file, tmp_path):
        """Global license should take precedence over project license"""
        project_license = tmp_path / "project_license.json"
        project_data = {
            "tier": "free"
        }
        with open(project_license, 'w') as f:
            json.dump(project_data, f)

        with patch.dict(os.environ, {}, clear=True):
            manager = LicenseManager()
            with patch.object(manager, 'GLOBAL_LICENSE_PATH', temp_license_file):
                with patch.object(manager, 'PROJECT_LICENSE_PATH', project_license):
                    license_info = manager.get_license()
                    # Should use global (pro), not project (free)
                    assert license_info.tier == LicenseTier.PROFESSIONAL


class TestLicenseExpiration:
    """Test license expiration handling"""

    def test_expired_license_is_invalid(self):
        """Expired paid license should be invalid"""
        expired_license = LicenseInfo(
            tier=LicenseTier.PROFESSIONAL,
            expires_at=datetime.now() - timedelta(days=1)
        )
        assert expired_license.is_valid is False

    def test_valid_license_with_future_expiration(self):
        """License expiring in future should be valid"""
        valid_license = LicenseInfo(
            tier=LicenseTier.PROFESSIONAL,
            expires_at=datetime.now() + timedelta(days=30)
        )
        assert valid_license.is_valid is True

    def test_license_expiring_today(self):
        """License expiring in a few hours should still be valid"""
        expiring_today = LicenseInfo(
            tier=LicenseTier.PROFESSIONAL,
            expires_at=datetime.now() + timedelta(hours=6)
        )
        assert expiring_today.is_valid is True
