#!/usr/bin/env python3
"""
RIGOROUS TEST SUITE - Evolution Cycle
======================================
Black-box and white-box tests for the main evolution cycle executor.
"""

import os
import sys
import json
import unittest
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock

# Add project root
sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from core.evolution.evolution_cycle import EvolutionCycle


class TestEvolutionCycleBlackBox(unittest.TestCase):
    """Black-box tests - external behavior."""

    def setUp(self):
        """Set up test fixtures."""
        self.temp_dir = tempfile.mkdtemp()
        self.cycle = EvolutionCycle(
            cycle_number=1,
            max_prds=2,
            surprise_threshold=0.5
        )
        # Override state file to temp location
        self.cycle.state_file = Path(self.temp_dir) / "evolution_state.json"

    def test_run_returns_dict(self):
        """T1: run() returns a dictionary."""
        result = self.cycle.run()

        self.assertIsInstance(result, dict)

    def test_run_contains_cycle_number(self):
        """T2: Result contains cycle_number."""
        result = self.cycle.run()

        self.assertIn('cycle_number', result)
        self.assertEqual(result['cycle_number'], 1)

    def test_run_contains_phases(self):
        """T3: Result contains phases dictionary."""
        result = self.cycle.run()

        self.assertIn('phases', result)
        self.assertIsInstance(result['phases'], dict)

    def test_run_contains_timestamps(self):
        """T4: Result contains start and end timestamps."""
        result = self.cycle.run()

        self.assertIn('start_time', result)
        self.assertIn('end_time', result)

    def test_run_contains_elapsed_seconds(self):
        """T5: Result contains elapsed_seconds."""
        result = self.cycle.run()

        self.assertIn('elapsed_seconds', result)
        self.assertIsInstance(result['elapsed_seconds'], float)

    def test_prds_generated_tracked(self):
        """T6: prds_generated is tracked."""
        self.cycle.run()

        self.assertGreaterEqual(self.cycle.prds_generated, 0)

    def test_stories_completed_tracked(self):
        """T7: stories_completed is tracked."""
        self.cycle.run()

        self.assertGreaterEqual(self.cycle.stories_completed, 0)

    def test_tests_created_tracked(self):
        """T8: tests_created is tracked."""
        self.cycle.run()

        self.assertGreaterEqual(self.cycle.tests_created, 0)

    def test_state_file_created(self):
        """T9: State file is created after run."""
        self.cycle.run()

        self.assertTrue(self.cycle.state_file.exists())

    def test_state_file_valid_json(self):
        """T10: State file contains valid JSON."""
        self.cycle.run()

        content = self.cycle.state_file.read_text()
        data = json.loads(content)  # Should not raise

        self.assertIn('cycles', data)


class TestEvolutionCycleWhiteBox(unittest.TestCase):
    """White-box tests - internal implementation."""

    def setUp(self):
        """Set up test fixtures."""
        self.temp_dir = tempfile.mkdtemp()
        self.cycle = EvolutionCycle(
            cycle_number=5,
            max_prds=3,
            surprise_threshold=0.4
        )
        self.cycle.state_file = Path(self.temp_dir) / "evolution_state.json"

    def test_phase_generate_returns_dict(self):
        """W1: _phase_generate returns dictionary with expected keys."""
        result = self.cycle._phase_generate()

        self.assertIsInstance(result, dict)
        self.assertIn('count', result)
        self.assertIn('prds', result)

    def test_phase_execute_returns_dict(self):
        """W2: _phase_execute returns dictionary with expected keys."""
        gen_result = self.cycle._phase_generate()
        result = self.cycle._phase_execute(gen_result.get('prds', []))

        self.assertIsInstance(result, dict)
        self.assertIn('stories', result)
        self.assertIn('results', result)

    def test_phase_verify_returns_dict(self):
        """W3: _phase_verify returns dictionary with expected keys."""
        gen_result = self.cycle._phase_generate()
        exec_result = self.cycle._phase_execute(gen_result.get('prds', []))
        result = self.cycle._phase_verify(exec_result)

        self.assertIsInstance(result, dict)
        self.assertIn('tests_created', result)

    def test_phase_learn_returns_dict(self):
        """W4: _phase_learn returns dictionary with expected keys."""
        result = self.cycle._phase_learn({'elapsed_seconds': 10, 'phases': {}})

        self.assertIsInstance(result, dict)
        self.assertIn('surprise_detected', result)
        self.assertIn('surprise_score', result)

    def test_save_state_creates_file(self):
        """W5: _save_state creates state file."""
        self.cycle._save_state({'test': True})

        self.assertTrue(self.cycle.state_file.exists())

    def test_save_state_appends_cycles(self):
        """W6: _save_state appends to cycles list."""
        self.cycle._save_state({'cycle': 1})
        self.cycle._save_state({'cycle': 2})

        data = json.loads(self.cycle.state_file.read_text())
        self.assertEqual(len(data['cycles']), 2)

    def test_save_state_limits_history(self):
        """W7: _save_state keeps only last 100 cycles."""
        # Manually create a large history
        state = {'cycles': [{'c': i} for i in range(150)]}
        self.cycle.state_file.parent.mkdir(parents=True, exist_ok=True)
        self.cycle.state_file.write_text(json.dumps(state))

        self.cycle._save_state({'cycle': 151})

        data = json.loads(self.cycle.state_file.read_text())
        self.assertLessEqual(len(data['cycles']), 100)

    def test_log_summary_outputs_metrics(self):
        """W8: _log_summary outputs expected metric strings."""
        self.cycle.prds_generated = 5
        self.cycle.stories_completed = 10
        self.cycle.tests_created = 20

        import io
        from contextlib import redirect_stdout

        f = io.StringIO()
        with redirect_stdout(f):
            self.cycle._log_summary()

        output = f.getvalue()
        self.assertIn('PRDs generated: 5', output)
        self.assertIn('Stories completed: 10', output)
        self.assertIn('Tests created: 20', output)

    def test_surprise_detection_in_learn_phase(self):
        """W9: _phase_learn can detect surprise."""
        # Create conditions that should trigger surprise
        cycle_results = {
            'elapsed_seconds': 500,  # Much longer than expected
            'phases': {
                'verify': {
                    'overall_pass_rate': 0.3  # Low pass rate
                }
            },
            'error': 'Something went wrong'
        }

        result = self.cycle._phase_learn(cycle_results)

        # With such deviation, surprise should be detected (depends on threshold)
        self.assertIn('surprise_score', result)


class TestEvolutionCycleIntegration(unittest.TestCase):
    """Integration tests - full cycle execution."""

    def setUp(self):
        """Set up test fixtures."""
        self.temp_dir = tempfile.mkdtemp()

    def test_full_cycle_completes(self):
        """I1: Full cycle completes without errors."""
        cycle = EvolutionCycle(cycle_number=1, max_prds=1)
        cycle.state_file = Path(self.temp_dir) / "state.json"

        result = cycle.run()

        self.assertNotIn('error', result)

    def test_multiple_cycles_accumulate_state(self):
        """I2: Multiple cycles accumulate state correctly."""
        state_file = Path(self.temp_dir) / "state.json"

        for i in range(3):
            cycle = EvolutionCycle(cycle_number=i + 1, max_prds=1)
            cycle.state_file = state_file
            cycle.run()

        data = json.loads(state_file.read_text())
        self.assertEqual(len(data['cycles']), 3)
        self.assertEqual(data['last_cycle'], 3)

    def test_different_max_prds_produces_different_counts(self):
        """I3: Different max_prds affects generation count."""
        cycle1 = EvolutionCycle(cycle_number=1, max_prds=1)
        cycle1.state_file = Path(self.temp_dir) / "state1.json"
        cycle1.run()

        cycle5 = EvolutionCycle(cycle_number=2, max_prds=5)
        cycle5.state_file = Path(self.temp_dir) / "state2.json"
        cycle5.run()

        # cycle5 should have generated more (though random stopping may affect this)
        # At minimum, max_prds=5 should allow for more PRDs than max_prds=1
        self.assertGreaterEqual(cycle5.prds_generated, 0)


class TestEvolutionCycleEdgeCases(unittest.TestCase):
    """Edge case tests."""

    def setUp(self):
        """Set up test fixtures."""
        self.temp_dir = tempfile.mkdtemp()

    def test_cycle_zero(self):
        """E1: Cycle number 0 is handled."""
        cycle = EvolutionCycle(cycle_number=0, max_prds=1)
        cycle.state_file = Path(self.temp_dir) / "state.json"

        result = cycle.run()

        self.assertEqual(result['cycle_number'], 0)

    def test_negative_cycle(self):
        """E2: Negative cycle number doesn't crash."""
        cycle = EvolutionCycle(cycle_number=-1, max_prds=1)
        cycle.state_file = Path(self.temp_dir) / "state.json"

        result = cycle.run()

        self.assertIsInstance(result, dict)

    def test_zero_surprise_threshold(self):
        """E3: Zero surprise threshold always detects surprise."""
        cycle = EvolutionCycle(cycle_number=1, max_prds=1, surprise_threshold=0.0)
        cycle.state_file = Path(self.temp_dir) / "state.json"

        result = cycle.run()

        # With threshold=0, any deviation triggers surprise
        self.assertIn('phases', result)

    def test_high_surprise_threshold(self):
        """E4: High surprise threshold rarely detects surprise."""
        cycle = EvolutionCycle(cycle_number=1, max_prds=1, surprise_threshold=1.0)
        cycle.state_file = Path(self.temp_dir) / "state.json"

        result = cycle.run()

        # With threshold=1.0, surprise should not be detected
        learn_result = result.get('phases', {}).get('learn', {})
        self.assertFalse(learn_result.get('surprise_detected', False))


if __name__ == '__main__':
    unittest.main(verbosity=2)
