#!/usr/bin/env python3
"""
Tests for Phase 9: Multi-Tenant/User Access
============================================
Comprehensive test suite covering:
- JWT authentication and token validation
- Role-based access control (admin/editor/viewer)
- Tenant data isolation
- Cross-tenant access prevention
- Error handling and security
"""

import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
import sys

# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))

from tools.factory import create_artifact, persist_artifact
from tools.artifacts import get_output_path


class TestAuthModule(unittest.TestCase):
    """Test authentication and authorization functions."""
    
    def test_password_hashing(self):
        """Test password hashing and verification."""
        try:
            from tools.auth import hash_password, verify_password, AUTH_AVAILABLE
            
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")
            
            password = "test_password_123"
            hashed = hash_password(password)
            
            # Verify hash is different from password
            self.assertNotEqual(password, hashed)
            self.assertGreater(len(hashed), 50)
            
            # Verify correct password
            self.assertTrue(verify_password(password, hashed))
            
            # Verify incorrect password
            self.assertFalse(verify_password("wrong_password", hashed))
            
        except ImportError:
            self.skipTest("Auth module not available")
    
    def test_jwt_token_creation(self):
        """Test JWT token creation and structure."""
        try:
            from tools.auth import create_access_token, AUTH_AVAILABLE
            
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")
            
            token = create_access_token(
                tenant_id="test-tenant",
                user_email="test@example.com",
                role="admin"
            )
            
            # Verify token is a non-empty string
            self.assertIsInstance(token, str)
            self.assertGreater(len(token), 100)
            
            # JWT tokens have 3 parts separated by dots
            parts = token.split('.')
            self.assertEqual(len(parts), 3)
            
        except ImportError:
            self.skipTest("Auth module not available")
    
    def test_jwt_token_verification(self):
        """Test JWT token verification and payload extraction."""
        try:
            from tools.auth import create_access_token, verify_token, AUTH_AVAILABLE
            
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")
            
            # Create token
            token = create_access_token(
                tenant_id="acme-corp",
                user_email="admin@acme.com",
                role="editor"
            )
            
            # Verify token
            payload = verify_token(token)
            
            self.assertIsNotNone(payload)
            self.assertEqual(payload["tenant_id"], "acme-corp")
            self.assertEqual(payload["sub"], "admin@acme.com")
            self.assertEqual(payload["role"], "editor")
            
        except ImportError:
            self.skipTest("Auth module not available")
    
    def test_jwt_invalid_token(self):
        """Test that invalid tokens are rejected."""
        try:
            from tools.auth import verify_token, AUTH_AVAILABLE
            
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")
            
            # Invalid token should return None
            payload = verify_token("invalid.token.here")
            self.assertIsNone(payload)
            
        except ImportError:
            self.skipTest("Auth module not available")
    
    def test_role_permissions(self):
        """Test role-based permission hierarchy."""
        try:
            from tools.auth import has_permission, AUTH_AVAILABLE
            
            if not AUTH_AVAILABLE:
                self.skipTest("Auth dependencies not available")
            
            # Admin has all permissions
            self.assertTrue(has_permission("admin", "viewer"))
            self.assertTrue(has_permission("admin", "editor"))
            self.assertTrue(has_permission("admin", "admin"))
            
            # Editor has editor and viewer permissions
            self.assertTrue(has_permission("editor", "viewer"))
            self.assertTrue(has_permission("editor", "editor"))
            self.assertFalse(has_permission("editor", "admin"))
            
            # Viewer only has viewer permissions
            self.assertTrue(has_permission("viewer", "viewer"))
            self.assertFalse(has_permission("viewer", "editor"))
            self.assertFalse(has_permission("viewer", "admin"))
            
        except ImportError:
            self.skipTest("Auth module not available")


class TestTenantIsolation(unittest.TestCase):
    """Test tenant data isolation and access control."""
    
    def setUp(self):
        """Create temporary tenant structure."""
        self.temp_dir = tempfile.TemporaryDirectory()
        self.test_dir = Path(self.temp_dir.name)
    
    def tearDown(self):
        """Clean up temporary directory."""
        self.temp_dir.cleanup()
    
    def test_tenant_subdirectory_creation(self):
        """Test that artifacts are stored in tenant subdirectories."""
        payload = {
            "title": "Tenant Test Decision",
            "context": "Testing tenant isolation",
            "options_considered": ["A", "B"],
            "chosen_option": "A",
            "reasoning": "Test",
            "constraints": [],
            "assumptions": [],
            "confidence": 0.9,
            "expected_outcome": "Success",
            "category": "test"
        }
        
        # Create artifact with tenant_id
        artifact = create_artifact("decision", payload, tenant_id="tenant-a")
        
        # Verify tenant_id is in artifact
        self.assertEqual(artifact.get("novyx:tenantId"), "tenant-a")
        self.assertEqual(artifact.get("tenant_id"), "tenant-a")
        
        # Persist and verify subdirectory
        path = persist_artifact(artifact, self.test_dir)
        
        # Path should include tenant subdirectory
        self.assertIn("tenant-a", str(path))
        self.assertTrue(path.parent.name == "tenant-a")
    
    def test_multiple_tenant_isolation(self):
        """Test that multiple tenants have separate subdirectories."""
        base_payload = {
            "title": "Isolation Test",
            "context": "Test",
            "options_considered": ["A"],
            "chosen_option": "A",
            "reasoning": "Test",
            "constraints": [],
            "assumptions": [],
            "confidence": 0.8,
            "expected_outcome": "Pass",
            "category": "test"
        }
        
        # Create artifacts for two different tenants
        for tenant in ["tenant-x", "tenant-y"]:
            artifact = create_artifact("decision", base_payload, tenant_id=tenant)
            path = persist_artifact(artifact, self.test_dir)
            
            # Verify correct tenant subdirectory
            self.assertEqual(path.parent.name, tenant)
        
        # Verify separate subdirectories exist
        self.assertTrue((self.test_dir / "tenant-x").exists())
        self.assertTrue((self.test_dir / "tenant-y").exists())
        
        # Verify each has one artifact
        tenant_x_files = list((self.test_dir / "tenant-x").glob("*.jsonld"))
        tenant_y_files = list((self.test_dir / "tenant-y").glob("*.jsonld"))
        
        self.assertEqual(len(tenant_x_files), 1)
        self.assertEqual(len(tenant_y_files), 1)
    
    def test_cli_tenant_filtering(self):
        """Test CLI tenant filtering functionality."""
        import subprocess
        
        # Create artifacts for multiple tenants
        for tenant in ["cli-tenant-1", "cli-tenant-2"]:
            for i in range(2):
                payload = {
                    "title": f"{tenant} Decision {i}",
                    "context": "CLI test",
                    "options_considered": ["A"],
                    "chosen_option": "A",
                    "reasoning": "Test",
                    "constraints": [],
                    "assumptions": [],
                    "confidence": 0.8,
                    "expected_outcome": "Pass",
                    "category": "cli-test"
                }
                artifact = create_artifact("decision", payload, tenant_id=tenant)
                persist_artifact(artifact, self.test_dir)
        
        # Test query with tenant filter
        result = subprocess.run(
            [sys.executable, "-m", "tools.cli", "query", 
             "--directory", str(self.test_dir), "--tenant", "cli-tenant-1"],
            capture_output=True,
            text=True,
            cwd=project_root
        )
        
        self.assertEqual(result.returncode, 0)
        self.assertIn("cli-tenant-1", result.stdout)
        self.assertIn("Total artifacts: 2", result.stdout)


class TestAPIAuthentication(unittest.TestCase):
    """Test API authentication endpoints and middleware."""

    def test_api_imports(self):
        """Test that API module imports correctly with auth."""
        try:
            from tools.api import app, JWT_ENABLED
            self.assertIsNotNone(app)
        except ImportError as e:
            self.skipTest(f"API module not available: {e}")
        except SystemExit:
            self.skipTest("API dependencies not installed (slowapi required)")

    def test_login_endpoint_exists(self):
        """Test that login endpoint is registered."""
        try:
            from tools.api import app

            # Check if login route exists
            routes = [route.path for route in app.routes]
            self.assertIn("/api/v1/auth/login", routes)
            self.assertIn("/api/v1/auth/me", routes)

        except ImportError:
            self.skipTest("API module not available")
        except SystemExit:
            self.skipTest("API dependencies not installed (slowapi required)")


class TestCrossTenantSecurity(unittest.TestCase):
    """Test that cross-tenant access is prevented."""
    
    def test_tenant_data_separation(self):
        """Test that tenant data is physically separated."""
        with tempfile.TemporaryDirectory() as tmpdir:
            base_dir = Path(tmpdir)
            
            # Create data for tenant A
            artifact_a = create_artifact(
                "decision",
                {
                    "title": "Tenant A Secret",
                    "context": "Confidential data for Tenant A",
                    "options_considered": ["A"],
                    "chosen_option": "A",
                    "reasoning": "Secret",
                    "constraints": [],
                    "assumptions": [],
                    "confidence": 1.0,
                    "expected_outcome": "Protected",
                    "category": "security"
                },
                tenant_id="tenant-a"
            )
            path_a = persist_artifact(artifact_a, base_dir)
            
            # Create data for tenant B
            artifact_b = create_artifact(
                "decision",
                {
                    "title": "Tenant B Secret",
                    "context": "Confidential data for Tenant B",
                    "options_considered": ["B"],
                    "chosen_option": "B",
                    "reasoning": "Secret",
                    "constraints": [],
                    "assumptions": [],
                    "confidence": 1.0,
                    "expected_outcome": "Protected",
                    "category": "security"
                },
                tenant_id="tenant-b"
            )
            path_b = persist_artifact(artifact_b, base_dir)
            
            # Verify paths are different
            self.assertNotEqual(path_a.parent, path_b.parent)
            
            # Verify tenant A cannot see tenant B's directory
            tenant_a_dir = base_dir / "tenant-a"
            tenant_b_dir = base_dir / "tenant-b"
            
            # List files in tenant A directory
            tenant_a_files = list(tenant_a_dir.rglob("*.jsonld"))
            
            # Verify tenant B data is not in tenant A's directory
            for file in tenant_a_files:
                with open(file, 'r') as f:
                    data = json.load(f)
                    self.assertEqual(data.get("tenant_id"), "tenant-a")
                    self.assertNotIn("Tenant B", data.get("title", ""))


class TestErrorHandling(unittest.TestCase):
    """Test error handling for multi-tenant operations."""
    
    def test_missing_tenant_directory(self):
        """Test graceful handling of missing tenant directory."""
        import subprocess
        
        result = subprocess.run(
            [sys.executable, "-m", "tools.cli", "query",
             "--directory", "memory/decisions", "--tenant", "nonexistent-tenant"],
            capture_output=True,
            text=True,
            cwd=project_root
        )
        
        # Should fail gracefully
        self.assertEqual(result.returncode, 1)
        self.assertIn("Directory not found", result.stdout)
    
    def test_artifact_without_tenant(self):
        """Test that artifacts without tenant_id don't break."""
        with tempfile.TemporaryDirectory() as tmpdir:
            payload = {
                "title": "No Tenant",
                "context": "Test",
                "options_considered": ["A"],
                "chosen_option": "A",
                "reasoning": "Test",
                "constraints": [],
                "assumptions": [],
                "confidence": 0.8,
                "expected_outcome": "Pass",
                "category": "test"
            }
            
            # Create without tenant_id (should work for backward compatibility)
            artifact = create_artifact("decision", payload)
            path = persist_artifact(artifact, Path(tmpdir))
            
            # Should persist directly in base directory (no subdirectory)
            self.assertEqual(path.parent, Path(tmpdir))


if __name__ == '__main__':
    unittest.main()
