#!/usr/bin/env python3
"""
Tests for Phase 13: Streaming & Batch Operations
=================================================
Comprehensive test suite covering:
- Streaming artifact loading (generator-based)
- Batch create/validate operations
- Multi-tenant aware streaming
- MAX_BATCH_SIZE enforcement
- Low-memory operation patterns
"""

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

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

from tools.batch import (
    stream_artifacts,
    stream_artifact_ids,
    batch_create,
    batch_validate,
    count_artifacts,
    is_streaming_available,
    MAX_BATCH_SIZE,
    DEFAULT_BATCH_SIZE,
    _detect_artifact_type,
    _normalize_type
)
from tools.factory import create_artifact, persist_artifact


class TestStreamArtifacts(unittest.TestCase):
    """Test streaming artifact loading."""

    def setUp(self):
        """Create temporary directory with test artifacts."""
        self.temp_dir = tempfile.TemporaryDirectory()
        self.test_dir = Path(self.temp_dir.name)

        # Create test artifacts
        self.artifact_count = 7
        for i in range(self.artifact_count):
            payload = {
                "title": f"Stream Test Decision {i}",
                "context": "Testing streaming",
                "options_considered": ["A", "B"],
                "chosen_option": "A",
                "reasoning": f"Reason {i}",
                "constraints": [],
                "assumptions": [],
                "confidence": 0.8,
                "expected_outcome": "Success",
                "category": "streaming-test"
            }
            artifact = create_artifact("decision", payload)
            persist_artifact(artifact, self.test_dir)

    def tearDown(self):
        """Clean up temporary directory."""
        self.temp_dir.cleanup()

    def test_stream_artifacts_batching(self):
        """Test that stream_artifacts yields correct batch sizes."""
        batch_size = 3
        batches = list(stream_artifacts(self.test_dir, batch_size=batch_size))

        # Should have 3 batches: [3, 3, 1]
        self.assertEqual(len(batches), 3)
        self.assertEqual(len(batches[0]), 3)
        self.assertEqual(len(batches[1]), 3)
        self.assertEqual(len(batches[2]), 1)

        # Total artifacts
        total = sum(len(b) for b in batches)
        self.assertEqual(total, self.artifact_count)

    def test_stream_artifacts_respects_max_batch_size(self):
        """Test that batch_size is capped at MAX_BATCH_SIZE."""
        # Request larger than max
        batches = list(stream_artifacts(self.test_dir, batch_size=MAX_BATCH_SIZE + 50))

        # Each batch should not exceed MAX_BATCH_SIZE
        for batch in batches:
            self.assertLessEqual(len(batch), MAX_BATCH_SIZE)

    def test_stream_artifacts_adds_metadata(self):
        """Test that streaming adds _path and _tenant metadata."""
        batches = list(stream_artifacts(self.test_dir, batch_size=10))

        self.assertGreater(len(batches), 0)
        for artifact in batches[0]:
            self.assertIn('_path', artifact)
            self.assertTrue(artifact['_path'].endswith('.jsonld'))

    def test_stream_artifacts_nonexistent_directory(self):
        """Test streaming from nonexistent directory yields empty."""
        batches = list(stream_artifacts(Path("/nonexistent/path")))
        self.assertEqual(len(batches), 0)


class TestStreamArtifactsTenant(unittest.TestCase):
    """Test multi-tenant streaming."""

    def setUp(self):
        """Create multi-tenant test structure."""
        self.temp_dir = tempfile.TemporaryDirectory()
        self.test_dir = Path(self.temp_dir.name)

        # Create artifacts for tenant-a (3 artifacts)
        for i in range(3):
            payload = {
                "title": f"Tenant A Decision {i}",
                "context": "Tenant A test",
                "options_considered": ["A"],
                "chosen_option": "A",
                "reasoning": "Tenant A",
                "constraints": [],
                "assumptions": [],
                "confidence": 0.9,
                "expected_outcome": "Pass",
                "category": "tenant-test"
            }
            artifact = create_artifact("decision", payload, tenant_id="tenant-a")
            persist_artifact(artifact, self.test_dir)

        # Create artifacts for tenant-b (2 artifacts)
        for i in range(2):
            payload = {
                "title": f"Tenant B Decision {i}",
                "context": "Tenant B test",
                "options_considered": ["B"],
                "chosen_option": "B",
                "reasoning": "Tenant B",
                "constraints": [],
                "assumptions": [],
                "confidence": 0.9,
                "expected_outcome": "Pass",
                "category": "tenant-test"
            }
            artifact = create_artifact("decision", payload, tenant_id="tenant-b")
            persist_artifact(artifact, self.test_dir)

    def tearDown(self):
        """Clean up temporary directory."""
        self.temp_dir.cleanup()

    def test_stream_artifacts_tenant_filter(self):
        """Test streaming with tenant filter."""
        # Stream only tenant-a
        batches = list(stream_artifacts(self.test_dir, tenant_id="tenant-a"))
        total = sum(len(b) for b in batches)
        self.assertEqual(total, 3)

        # Stream only tenant-b
        batches = list(stream_artifacts(self.test_dir, tenant_id="tenant-b"))
        total = sum(len(b) for b in batches)
        self.assertEqual(total, 2)

    def test_stream_artifacts_all_tenants(self):
        """Test streaming without tenant filter gets all."""
        # Note: Without tenant_id, it scans from base which includes subdirectories
        batches = list(stream_artifacts(self.test_dir))
        total = sum(len(b) for b in batches)
        self.assertEqual(total, 5)


class TestBatchCreate(unittest.TestCase):
    """Test batch artifact creation."""

    def setUp(self):
        """Create temporary directory."""
        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_batch_create_success(self):
        """Test successful batch creation."""
        artifacts = [
            {
                "@type": "novyx:Decision",
                "title": f"Batch Decision {i}",
                "context": "Batch test",
                "options_considered": ["A"],
                "chosen_option": "A",
                "reasoning": "Batch create test",
                "constraints": [],
                "assumptions": [],
                "confidence": 0.85,
                "expected_outcome": "Created",
                "category": "batch-test"
            }
            for i in range(5)
        ]

        result = batch_create(artifacts, directory=self.test_dir)

        self.assertEqual(result['created'], 5)
        self.assertEqual(result['failed'], 0)
        self.assertEqual(len(result['results']), 5)
        self.assertEqual(len(result['errors']), 0)

        # Verify files exist
        files = list(self.test_dir.rglob("*.jsonld"))
        self.assertEqual(len(files), 5)

    def test_batch_create_empty_list(self):
        """Test batch create with empty list."""
        result = batch_create([], directory=self.test_dir)

        self.assertEqual(result['created'], 0)
        self.assertEqual(result['failed'], 0)

    def test_batch_create_exceeds_max(self):
        """Test batch create rejects oversized batches."""
        artifacts = [{"@type": "novyx:Decision"} for _ in range(MAX_BATCH_SIZE + 1)]

        with self.assertRaises(ValueError) as ctx:
            batch_create(artifacts, directory=self.test_dir)

        self.assertIn("exceeds maximum", str(ctx.exception))

    def test_batch_create_with_invalid_type(self):
        """Test batch create handles invalid artifact types."""
        artifacts = [
            {
                "@type": "novyx:UnknownType",
                "title": "Invalid Type Test"
            }
        ]

        result = batch_create(artifacts, directory=self.test_dir)

        self.assertEqual(result['created'], 0)
        self.assertEqual(result['failed'], 1)
        # Error can be either "Unknown artifact type" or "Could not determine"
        error_msg = result['errors'][0]['error']
        self.assertTrue(
            "Unknown artifact type" in error_msg or "Could not determine" in error_msg,
            f"Unexpected error: {error_msg}"
        )


class TestBatchValidate(unittest.TestCase):
    """Test streaming batch validation."""

    def setUp(self):
        """Create test artifacts for validation."""
        self.temp_dir = tempfile.TemporaryDirectory()
        self.test_dir = Path(self.temp_dir.name)

        # Create valid artifacts
        for i in range(5):
            payload = {
                "title": f"Validate Test {i}",
                "context": "Validation test",
                "options_considered": ["A"],
                "chosen_option": "A",
                "reasoning": "Test",
                "constraints": [],
                "assumptions": [],
                "confidence": 0.8,
                "expected_outcome": "Valid",
                "category": "validation-test"
            }
            artifact = create_artifact("decision", payload)
            persist_artifact(artifact, self.test_dir)

    def tearDown(self):
        """Clean up temporary directory."""
        self.temp_dir.cleanup()

    def test_batch_validate_streaming(self):
        """Test streaming validation yields batch results."""
        results = list(batch_validate(self.test_dir, batch_size=2))

        # Should have multiple batches
        self.assertGreater(len(results), 1)

        # Each result should have expected keys
        for result in results:
            self.assertIn('batch_size', result)
            self.assertIn('passed', result)
            self.assertIn('failed', result)
            self.assertIn('errors', result)

        # Total should match artifact count
        total_passed = sum(r['passed'] for r in results)
        total_failed = sum(r['failed'] for r in results)
        self.assertEqual(total_passed + total_failed, 5)


class TestCountArtifacts(unittest.TestCase):
    """Test artifact counting."""

    def test_count_artifacts(self):
        """Test counting artifacts in directory."""
        with tempfile.TemporaryDirectory() as tmpdir:
            test_dir = Path(tmpdir)

            # Create 4 artifacts
            for i in range(4):
                payload = {
                    "title": f"Count Test {i}",
                    "context": "Count test",
                    "options_considered": ["A"],
                    "chosen_option": "A",
                    "reasoning": "Test",
                    "constraints": [],
                    "assumptions": [],
                    "confidence": 0.8,
                    "expected_outcome": "Counted",
                    "category": "count-test"
                }
                artifact = create_artifact("decision", payload)
                persist_artifact(artifact, test_dir)

            count = count_artifacts(test_dir)
            self.assertEqual(count, 4)

    def test_count_artifacts_nonexistent(self):
        """Test counting in nonexistent directory."""
        count = count_artifacts(Path("/nonexistent/path"))
        self.assertEqual(count, 0)


class TestStreamArtifactIds(unittest.TestCase):
    """Test ID-only streaming."""

    def test_stream_artifact_ids(self):
        """Test streaming artifact IDs without full content."""
        with tempfile.TemporaryDirectory() as tmpdir:
            test_dir = Path(tmpdir)

            # Create artifacts
            created_ids = []
            for i in range(3):
                payload = {
                    "title": f"ID Stream Test {i}",
                    "context": "ID streaming test",
                    "options_considered": ["A"],
                    "chosen_option": "A",
                    "reasoning": "Test",
                    "constraints": [],
                    "assumptions": [],
                    "confidence": 0.8,
                    "expected_outcome": "ID extracted",
                    "category": "id-stream-test"
                }
                artifact = create_artifact("decision", payload)
                created_ids.append(artifact['uuid'])
                persist_artifact(artifact, test_dir)

            # Stream IDs
            ids = list(stream_artifact_ids(test_dir))
            self.assertEqual(len(ids), 3)

            # Each result should be (id, path) tuple
            for artifact_id, path in ids:
                self.assertIn(artifact_id, created_ids)
                self.assertTrue(path.exists())


class TestUtilityFunctions(unittest.TestCase):
    """Test utility functions."""

    def test_is_streaming_available(self):
        """Test streaming availability check."""
        result = is_streaming_available()
        self.assertIsInstance(result, bool)

    def test_detect_artifact_type_string(self):
        """Test type detection from string @type."""
        payload = {"@type": "novyx:Decision"}
        result = _detect_artifact_type(payload)
        self.assertEqual(result, "decision")

    def test_detect_artifact_type_array(self):
        """Test type detection from array @type."""
        payload = {"@type": ["schema:DigitalDocument", "novyx:Decision"]}
        result = _detect_artifact_type(payload)
        self.assertEqual(result, "decision")

    def test_detect_artifact_type_missing(self):
        """Test type detection with missing @type."""
        payload = {"title": "No type"}
        result = _detect_artifact_type(payload)
        self.assertIsNone(result)

    def test_normalize_type(self):
        """Test type normalization."""
        self.assertEqual(_normalize_type("novyx:Decision"), "decision")
        self.assertEqual(_normalize_type("Decision"), "decision")
        self.assertEqual(_normalize_type("DECISION"), "decision")
        self.assertEqual(_normalize_type("ApiEndpoint"), "apiendpoint")
        self.assertIsNone(_normalize_type("UnknownType"))
        self.assertIsNone(_normalize_type(""))
        self.assertIsNone(_normalize_type(None))


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