#!/usr/bin/env python3
"""
tests/test_versioning.py

Phase 10 Test Suite: Artifact Versioning & History

Tests covering:
- Version creation and persistence
- Diff computation and application
- Version listing and retrieval
- Rollback functionality
- Integrity verification
- Multi-tenant version isolation
- Factory integration
- CLI version commands
- API version endpoints
"""

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

# Add parent directory to path for imports
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))

from tools.versioning import (
    create_version,
    persist_version,
    list_versions,
    get_version,
    rollback_to_version,
    compute_diff,
    apply_diff,
    get_latest_version_number
)
from tools.factory import persist_artifact
from tools.artifacts import get_artifact_config


class TestVersionCreation(unittest.TestCase):
    """Test version artifact creation and diff computation."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.artifact_v1 = {
            "uuid": "urn:uuid:test-artifact-001",
            "@type": ["novyx:Decision", "schema:CreativeWork"],
            "title": "Test Decision",
            "reasoning": "Initial reasoning",
            "confidence": 0.7,
            "constraints": ["Must be fast"]
        }
        
        self.artifact_v2 = {
            **self.artifact_v1,
            "reasoning": "Enhanced reasoning with more details",
            "confidence": 0.8,
            "constraints": ["Must be fast", "Must scale"]
        }
    
    def test_compute_diff_creates_unified_diff(self):
        """Test that compute_diff creates a valid unified diff."""
        old_json = json.dumps(self.artifact_v1, indent=2, sort_keys=True)
        new_json = json.dumps(self.artifact_v2, indent=2, sort_keys=True)
        
        diff = compute_diff(old_json, new_json, "urn:uuid:test-artifact-001")
        
        self.assertIn("---", diff)
        self.assertIn("+++", diff)
        self.assertIn("reasoning", diff)
        self.assertIn("confidence", diff)
    
    def test_create_version_for_initial_artifact(self):
        """Test creating version 1 (initial version with snapshot)."""
        version = create_version(
            current_artifact=self.artifact_v1,
            parent_artifact=None,
            change_summary="Initial version",
            changed_by="test-user",
            change_type="create"
        )
        
        self.assertEqual(version["artifact_id"], "urn:uuid:test-artifact-001")
        self.assertEqual(version["version_number"], 1)
        self.assertEqual(version["change_summary"], "Initial version")
        self.assertEqual(version["changed_by"], "test-user")
        self.assertIsNotNone(version.get("artifact_snapshot"))
        self.assertEqual(version["diff"], "")
    
    def test_create_version_for_update(self):
        """Test creating version 2+ with diff."""
        # Mark v1 with metadata
        v1_with_meta = {**self.artifact_v1, "_version_number": 1, "_version_id": "urn:uuid:v1"}
        
        version = create_version(
            current_artifact=self.artifact_v2,
            parent_artifact=v1_with_meta,
            change_summary="Enhanced reasoning",
            changed_by="test-user",
            change_type="update"
        )
        
        self.assertEqual(version["version_number"], 2)
        self.assertNotEqual(version["diff"], "")
        self.assertIn("reasoning", version["diff"])
        self.assertIsNone(version.get("artifact_snapshot"))


class TestVersionPersistenceAndListing(unittest.TestCase):
    """Test version persistence and retrieval."""
    
    def setUp(self):
        """Set up temporary directory for test versions."""
        self.temp_dir = tempfile.mkdtemp()
        self.versions_dir = Path(self.temp_dir) / "versions"
        self.versions_dir.mkdir(parents=True)
        
        self.artifact_id = "urn:uuid:test-artifact-002"
    
    def tearDown(self):
        """Clean up temporary directory."""
        shutil.rmtree(self.temp_dir, ignore_errors=True)
    
    def test_persist_version_creates_file(self):
        """Test that persist_version creates a version file."""
        version = {
            "uuid": "urn:uuid:version-001",
            "@type": ["novyx:Version"],
            "artifact_id": self.artifact_id,
            "version_number": 1,
            "diff": "",
            "change_summary": "Initial version",
            "createdAt": datetime.now(timezone.utc).isoformat()
        }
        
        version_path = persist_version(version, self.artifact_id, versions_dir=self.versions_dir)
        
        self.assertTrue(version_path.exists())
        self.assertTrue(version_path.name.endswith(".jsonld"))
        
        # Verify content
        with open(version_path, 'r') as f:
            saved_version = json.load(f)
        self.assertEqual(saved_version["artifact_id"], self.artifact_id)
    
    def test_list_versions_returns_empty_for_no_versions(self):
        """Test list_versions returns empty list for artifact with no versions."""
        versions = list_versions(self.artifact_id, versions_dir=self.versions_dir)
        self.assertEqual(versions, [])
    
    def test_list_versions_returns_sorted_versions(self):
        """Test list_versions returns versions sorted by version_number."""
        # Create 3 versions
        for i in range(1, 4):
            version = {
                "uuid": f"urn:uuid:version-00{i}",
                "@type": ["novyx:Version"],
                "artifact_id": self.artifact_id,
                "version_number": i,
                "diff": "",
                "change_summary": f"Version {i}",
                "createdAt": datetime.now(timezone.utc).isoformat()
            }
            persist_version(version, self.artifact_id, versions_dir=self.versions_dir)
        
        versions = list_versions(self.artifact_id, versions_dir=self.versions_dir)
        
        self.assertEqual(len(versions), 3)
        self.assertEqual(versions[0]["version_number"], 1)
        self.assertEqual(versions[1]["version_number"], 2)
        self.assertEqual(versions[2]["version_number"], 3)
    
    def test_get_version_retrieves_specific_version(self):
        """Test get_version retrieves a specific version by number."""
        # Create version 1
        version = {
            "uuid": "urn:uuid:version-001",
            "@type": ["novyx:Version"],
            "artifact_id": self.artifact_id,
            "version_number": 1,
            "diff": "",
            "change_summary": "Initial version",
            "createdAt": datetime.now(timezone.utc).isoformat()
        }
        persist_version(version, self.artifact_id, versions_dir=self.versions_dir)
        
        retrieved = get_version(self.artifact_id, 1, versions_dir=self.versions_dir)
        
        self.assertIsNotNone(retrieved)
        self.assertEqual(retrieved["version_number"], 1)
        self.assertEqual(retrieved["artifact_id"], self.artifact_id)
    
    def test_get_version_returns_none_for_missing_version(self):
        """Test get_version returns None for non-existent version."""
        retrieved = get_version(self.artifact_id, 99, versions_dir=self.versions_dir)
        self.assertIsNone(retrieved)


class TestRollbackFunctionality(unittest.TestCase):
    """Test artifact rollback capabilities."""
    
    def setUp(self):
        """Set up test fixtures with version history."""
        self.temp_dir = tempfile.mkdtemp()
        self.versions_dir = Path(self.temp_dir) / "versions"
        self.versions_dir.mkdir(parents=True)
        
        self.artifact_id = "urn:uuid:test-artifact-003"
        
        self.artifact_v1 = {
            "uuid": self.artifact_id,
            "@type": ["novyx:Decision"],
            "title": "Test Decision",
            "reasoning": "Initial reasoning",
            "confidence": 0.7
        }
        
        # Create version 1 with snapshot
        version1 = {
            "uuid": "urn:uuid:version-001",
            "@type": ["novyx:Version"],
            "artifact_id": self.artifact_id,
            "version_number": 1,
            "artifact_snapshot": self.artifact_v1,
            "diff": "",
            "change_summary": "Initial version",
            "createdAt": datetime.now(timezone.utc).isoformat()
        }
        
        # Create artifact directory and persist version
        artifact_dir = self.versions_dir / self.artifact_id.replace("urn:uuid:", "")
        artifact_dir.mkdir(parents=True)
        
        with open(artifact_dir / "version_001.jsonld", 'w') as f:
            json.dump(version1, f, indent=2)
    
    def tearDown(self):
        """Clean up temporary directory."""
        shutil.rmtree(self.temp_dir, ignore_errors=True)
    
    @patch('tools.versioning.Path')
    def test_rollback_to_version_1_returns_snapshot(self, mock_path):
        """Test rollback_to_version returns the snapshot for version 1."""
        # Mock Path to use our temp directory
        def mock_path_init(path_str):
            if path_str == "memory/versions":
                return self.versions_dir
            return Path(path_str)
        
        mock_path.side_effect = mock_path_init
        
        restored = rollback_to_version(self.artifact_id, 1)
        
        self.assertIsNotNone(restored)
        self.assertEqual(restored["uuid"], self.artifact_id)
        self.assertEqual(restored["confidence"], 0.7)
    
    def test_rollback_to_missing_version_returns_none(self):
        """Test rollback_to_version returns None for non-existent version."""
        restored = rollback_to_version("urn:uuid:nonexistent", 1)
        self.assertIsNone(restored)


class TestFactoryIntegration(unittest.TestCase):
    """Test versioning integration with factory.persist_artifact."""
    
    def setUp(self):
        """Set up temporary directory for artifacts."""
        self.temp_dir = tempfile.mkdtemp()
        self.artifacts_dir = Path(self.temp_dir) / "artifacts"
        self.artifacts_dir.mkdir(parents=True)
    
    def tearDown(self):
        """Clean up temporary directory."""
        shutil.rmtree(self.temp_dir, ignore_errors=True)
    
    def test_persist_artifact_creates_version_on_update(self):
        """Test that persist_artifact creates a version when updating an artifact."""
        artifact = {
            "uuid": "urn:uuid:test-artifact-004",
            "@type": ["novyx:Decision"],
            "title": "Test Decision",
            "reasoning": "Initial reasoning",
            "createdAt": datetime.now(timezone.utc).isoformat(),
            "updatedAt": datetime.now(timezone.utc).isoformat()
        }
        
        # First persist (create)
        path1 = persist_artifact(artifact, self.artifacts_dir)
        self.assertTrue(path1.exists())
        
        # Update artifact
        artifact["reasoning"] = "Updated reasoning"
        
        # Second persist (update) - should create a version
        with patch('tools.factory.Path', return_value=Path(self.temp_dir) / "versions"):
            path2 = persist_artifact(artifact, self.artifacts_dir)
        
        self.assertTrue(path2.exists())
        # The path should be different if hash changed
        # (or same if hash somehow didn't change)
    
    def test_persist_artifact_skips_versioning_when_flag_set(self):
        """Test that persist_artifact skips versioning when skip_versioning=True."""
        artifact = {
            "uuid": "urn:uuid:test-artifact-005",
            "@type": ["novyx:Decision"],
            "title": "Test Decision",
            "reasoning": "Initial reasoning",
            "createdAt": datetime.now(timezone.utc).isoformat(),
            "updatedAt": datetime.now(timezone.utc).isoformat()
        }
        
        # Persist with skip_versioning=True
        path = persist_artifact(artifact, self.artifacts_dir, skip_versioning=True)
        
        self.assertTrue(path.exists())


class TestMultiTenantVersioning(unittest.TestCase):
    """Test version isolation across tenants."""
    
    def setUp(self):
        """Set up temporary directories for multi-tenant versions."""
        self.temp_dir = tempfile.mkdtemp()
        self.versions_dir = Path(self.temp_dir) / "versions"
        self.versions_dir.mkdir(parents=True)
        
        self.artifact_id = "urn:uuid:test-artifact-006"
        self.tenant_a = "tenant-a"
        self.tenant_b = "tenant-b"
    
    def tearDown(self):
        """Clean up temporary directory."""
        shutil.rmtree(self.temp_dir, ignore_errors=True)
    
    def test_list_versions_filters_by_tenant(self):
        """Test that list_versions only returns versions for the specified tenant."""
        # Create versions for tenant-a
        tenant_a_dir = self.versions_dir / self.tenant_a
        tenant_a_dir.mkdir(parents=True)
        
        version_a = {
            "uuid": "urn:uuid:version-a1",
            "@type": ["novyx:Version"],
            "artifact_id": self.artifact_id,
            "version_number": 1,
            "diff": "",
            "change_summary": "Tenant A version",
            "createdAt": datetime.now(timezone.utc).isoformat()
        }
        
        artifact_dir_a = tenant_a_dir / self.artifact_id.replace("urn:uuid:", "")
        artifact_dir_a.mkdir(parents=True)
        with open(artifact_dir_a / "version_a1.jsonld", 'w') as f:
            json.dump(version_a, f, indent=2)
        
        # Create versions for tenant-b
        tenant_b_dir = self.versions_dir / self.tenant_b
        tenant_b_dir.mkdir(parents=True)
        
        version_b = {
            "uuid": "urn:uuid:version-b1",
            "@type": ["novyx:Version"],
            "artifact_id": self.artifact_id,
            "version_number": 1,
            "diff": "",
            "change_summary": "Tenant B version",
            "createdAt": datetime.now(timezone.utc).isoformat()
        }
        
        artifact_dir_b = tenant_b_dir / self.artifact_id.replace("urn:uuid:", "")
        artifact_dir_b.mkdir(parents=True)
        with open(artifact_dir_b / "version_b1.jsonld", 'w') as f:
            json.dump(version_b, f, indent=2)
        
        # List versions for tenant-a
        versions_a = list_versions(self.artifact_id, versions_dir=tenant_a_dir)
        self.assertEqual(len(versions_a), 1)
        self.assertEqual(versions_a[0]["change_summary"], "Tenant A version")
        
        # List versions for tenant-b
        versions_b = list_versions(self.artifact_id, versions_dir=tenant_b_dir)
        self.assertEqual(len(versions_b), 1)
        self.assertEqual(versions_b[0]["change_summary"], "Tenant B version")


class TestIntegrityAndErrorHandling(unittest.TestCase):
    """Test integrity checks and error handling."""
    
    def test_create_version_requires_artifact_id(self):
        """Test that create_version raises error if artifact lacks ID."""
        artifact = {
            "@type": ["novyx:Decision"],
            "title": "No ID"
        }
        
        with self.assertRaises(ValueError):
            create_version(artifact, None, "Test", "test-user", "create")
    
    def test_compute_diff_handles_empty_strings(self):
        """Test that compute_diff handles empty input gracefully."""
        diff = compute_diff("", "", "urn:uuid:test")
        self.assertIsInstance(diff, str)
    
    def test_get_latest_version_number_returns_zero_for_no_versions(self):
        """Test get_latest_version_number returns 0 when no versions exist."""
        latest = get_latest_version_number("urn:uuid:nonexistent")
        self.assertEqual(latest, 0)


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