"""
End-to-end integration tests for research workflow.

Tests cover:
- Complete research flow from query to report
- Research mode variations (quick, deep)
- Settings propagation through phases
- Database persistence and retrieval
- Export functionality
- Research lifecycle management
"""

import json
import time
from unittest.mock import Mock
from datetime import datetime


class TestResearchQueryValidation:
    """Tests for research query validation."""

    def test_valid_query_accepted(self):
        """Valid queries should be accepted."""
        valid_queries = [
            "What is machine learning?",
            "Explain quantum computing",
            "History of artificial intelligence",
            "比较不同的编程语言",  # Chinese
            "¿Qué es la inteligencia artificial?",  # Spanish
        ]

        def validate_query(query):
            if not query or not query.strip():
                return False, "Query cannot be empty"
            if len(query) > 10000:
                return False, "Query too long"
            return True, None

        for query in valid_queries:
            is_valid, error = validate_query(query)
            assert is_valid is True, f"Query '{query}' should be valid"

    def test_empty_query_rejected(self):
        """Empty queries should be rejected."""
        invalid_queries = ["", "   ", "\t\n", None]

        def validate_query(query):
            if not query or not query.strip():
                return False, "Query cannot be empty"
            return True, None

        for query in invalid_queries:
            if query is None:
                is_valid = False
            else:
                is_valid, _ = validate_query(query)
            assert is_valid is False

    def test_query_length_limits(self):
        """Query length should be limited."""
        max_length = 10000

        def validate_query(query):
            if len(query) > max_length:
                return False, "Query too long"
            return True, None

        # Just under limit
        is_valid, _ = validate_query("x" * 10000)
        assert is_valid is True

        # Over limit
        is_valid, _ = validate_query("x" * 10001)
        assert is_valid is False

    def test_query_sanitization(self):
        """Queries should be sanitized."""

        def sanitize_query(query):
            # Remove control characters
            sanitized = "".join(
                c for c in query if c.isprintable() or c in "\n\t"
            )
            # Normalize whitespace
            sanitized = " ".join(sanitized.split())
            return sanitized

        query = "What   is\t\nAI?\x00\x01"
        sanitized = sanitize_query(query)
        assert "\x00" not in sanitized
        assert "What is AI?" == sanitized


class TestResearchModeSelection:
    """Tests for research mode selection."""

    def test_quick_mode_configuration(self):
        """Quick mode should have correct configuration."""
        quick_config = {
            "mode": "quick",
            "max_iterations": 1,
            "max_sources": 10,
            "synthesis_depth": "shallow",
        }

        assert quick_config["max_iterations"] == 1

    def test_deep_mode_configuration(self):
        """Deep mode should have correct configuration."""
        deep_config = {
            "mode": "deep",
            "max_iterations": 5,
            "max_sources": 50,
            "synthesis_depth": "comprehensive",
        }

        assert deep_config["max_iterations"] == 5

    def test_mode_selection_from_settings(self):
        """Mode should be selected from settings."""
        settings = {"research.mode": "deep"}

        def get_mode_config(settings):
            mode = settings.get("research.mode", "quick")
            configs = {
                "quick": {"iterations": 1, "depth": 1},
                "deep": {"iterations": 5, "depth": 3},
            }
            return configs.get(mode, configs["quick"])

        config = get_mode_config(settings)
        assert config["iterations"] == 5


class TestResearchPhaseExecution:
    """Tests for research phase execution."""

    def test_analysis_phase_execution(self):
        """Analysis phase should execute correctly."""
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(
            content="Analysis result: The topic involves..."
        )

        def run_analysis(query, llm):
            response = llm.invoke(f"Analyze: {query}")
            return {"phase": "analysis", "result": response.content}

        result = run_analysis("test query", mock_llm)
        assert result["phase"] == "analysis"
        assert "Analysis result" in result["result"]

    def test_search_phase_execution(self):
        """Search phase should execute correctly."""
        mock_search = Mock()
        mock_search.search.return_value = [
            {"title": "Result 1", "url": "http://example1.com"},
            {"title": "Result 2", "url": "http://example2.com"},
        ]

        def run_search(query, search_engine):
            results = search_engine.search(query)
            return {"phase": "search", "results": results}

        result = run_search("test query", mock_search)
        assert result["phase"] == "search"
        assert len(result["results"]) == 2

    def test_synthesis_phase_execution(self):
        """Synthesis phase should execute correctly."""
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(
            content="# Research Report\n\n## Summary\n\nFindings..."
        )

        def run_synthesis(analysis, search_results, llm):
            prompt = (
                f"Synthesize: {analysis} with {len(search_results)} sources"
            )
            response = llm.invoke(prompt)
            return {"phase": "synthesis", "report": response.content}

        result = run_synthesis(
            "analysis data", ["source1", "source2"], mock_llm
        )
        assert result["phase"] == "synthesis"
        assert "# Research Report" in result["report"]


class TestResearchProgressTracking:
    """Tests for research progress tracking."""

    def test_progress_updates_sequentially(self):
        """Progress should update sequentially."""
        progress_history = []

        def update_progress(phase, percentage, message):
            progress_history.append(
                {"phase": phase, "percentage": percentage, "message": message}
            )

        update_progress("initialization", 5, "Starting research...")
        update_progress("analysis", 20, "Analyzing query...")
        update_progress("search", 50, "Searching sources...")
        update_progress("synthesis", 80, "Generating report...")
        update_progress("complete", 100, "Research complete")

        assert len(progress_history) == 5
        # Percentages should increase
        percentages = [p["percentage"] for p in progress_history]
        assert percentages == sorted(percentages)

    def test_progress_callbacks_invoked(self):
        """Progress callbacks should be invoked."""
        callback_invocations = []

        def progress_callback(data):
            callback_invocations.append(data)

        # Simulate research with callbacks
        phases = ["init", "analyze", "search", "synthesize", "complete"]
        for i, phase in enumerate(phases):
            progress_callback(
                {"phase": phase, "progress": (i + 1) / len(phases) * 100}
            )

        assert len(callback_invocations) == 5


class TestResearchDatabasePersistence:
    """Tests for research database persistence."""

    def test_research_saved_to_database(self):
        """Research should be saved to database."""
        mock_db = {}

        def save_research(research_id, data):
            mock_db[research_id] = {
                "id": research_id,
                "query": data["query"],
                "status": data["status"],
                "created_at": datetime.now().isoformat(),
            }

        save_research(
            "research_1", {"query": "test query", "status": "completed"}
        )

        assert "research_1" in mock_db
        assert mock_db["research_1"]["query"] == "test query"

    def test_research_retrievable_by_id(self):
        """Research should be retrievable by ID."""
        mock_db = {
            "research_1": {"id": "research_1", "query": "test query"},
        }

        def get_research(research_id):
            return mock_db.get(research_id)

        result = get_research("research_1")
        assert result is not None
        assert result["query"] == "test query"

    def test_research_status_updates_persisted(self):
        """Research status updates should be persisted."""
        mock_db = {
            "research_1": {"status": "in_progress"},
        }

        def update_status(research_id, new_status):
            if research_id in mock_db:
                mock_db[research_id]["status"] = new_status
                mock_db[research_id]["updated_at"] = datetime.now().isoformat()

        update_status("research_1", "completed")

        assert mock_db["research_1"]["status"] == "completed"
        assert "updated_at" in mock_db["research_1"]


class TestResearchReportGeneration:
    """Tests for research report generation."""

    def test_markdown_report_generated(self):
        """Markdown report should be generated."""
        synthesis = "Research findings summary"
        sources = [{"title": "Source 1", "url": "http://example.com"}]

        def generate_markdown_report(synthesis, sources):
            report = f"# Research Report\n\n{synthesis}\n\n## Sources\n\n"
            for source in sources:
                report += f"- [{source['title']}]({source['url']})\n"
            return report

        report = generate_markdown_report(synthesis, sources)
        assert "# Research Report" in report
        assert "Research findings summary" in report

    def test_report_includes_metadata(self):
        """Report should include metadata."""

        def generate_report_with_metadata(content, metadata):
            return {
                "content": content,
                "metadata": {
                    "generated_at": datetime.now().isoformat(),
                    "query": metadata.get("query"),
                    "mode": metadata.get("mode"),
                    "source_count": metadata.get("source_count"),
                },
            }

        report = generate_report_with_metadata(
            "Report content",
            {"query": "test", "mode": "quick", "source_count": 5},
        )

        assert "metadata" in report
        assert report["metadata"]["query"] == "test"


class TestResearchExport:
    """Tests for research export functionality."""

    def test_export_to_markdown(self):
        """Should export research to markdown."""

        def export_markdown(report):
            return f"# {report['title']}\n\n{report['content']}"

        report = {"title": "Test Report", "content": "Report content here"}
        exported = export_markdown(report)

        assert "# Test Report" in exported

    def test_export_to_json(self):
        """Should export research to JSON."""

        def export_json(report):
            return json.dumps(report, indent=2)

        report = {"title": "Test Report", "content": "Report content"}
        exported = export_json(report)

        assert "Test Report" in exported
        # Should be valid JSON
        parsed = json.loads(exported)
        assert parsed["title"] == "Test Report"

    def test_export_to_html(self):
        """Should export research to HTML."""

        def export_html(report):
            return f"""
            <!DOCTYPE html>
            <html>
            <head><title>{report["title"]}</title></head>
            <body>
            <h1>{report["title"]}</h1>
            <div>{report["content"]}</div>
            </body>
            </html>
            """

        report = {"title": "Test Report", "content": "Report content"}
        exported = export_html(report)

        assert "<html>" in exported
        assert "Test Report" in exported


class TestResearchCancellation:
    """Tests for research cancellation."""

    def test_research_can_be_cancelled(self):
        """Research should be cancellable."""
        research = {
            "id": "research_1",
            "status": "in_progress",
            "cancelled": False,
        }

        def cancel_research(research):
            research["cancelled"] = True
            research["status"] = "cancelled"
            return research

        cancelled = cancel_research(research)

        assert cancelled["status"] == "cancelled"
        assert cancelled["cancelled"] is True

    def test_cancellation_stops_processing(self):
        """Cancellation should stop processing."""
        research = {"cancelled": False}
        processed_phases = []

        def process_phase(name, research):
            if research["cancelled"]:
                return False
            processed_phases.append(name)
            return True

        # Process some phases
        process_phase("analysis", research)
        process_phase("search", research)

        # Cancel
        research["cancelled"] = True

        # Should not process more
        result = process_phase("synthesis", research)

        assert result is False
        assert "synthesis" not in processed_phases


class TestResearchTimeout:
    """Tests for research timeout handling."""

    def test_research_timeout_detected(self):
        """Research timeout should be detected."""
        research = {
            "started_at": time.time() - 400,  # 400 seconds ago
            "timeout": 300,  # 5 minute timeout
        }

        def is_timed_out(research):
            elapsed = time.time() - research["started_at"]
            return elapsed > research["timeout"]

        assert is_timed_out(research) is True

    def test_timeout_triggers_cleanup(self):
        """Timeout should trigger cleanup."""
        cleanup_called = []

        def handle_timeout(research_id):
            cleanup_called.append(research_id)
            return {"status": "timeout", "id": research_id}

        result = handle_timeout("research_1")

        assert "research_1" in cleanup_called
        assert result["status"] == "timeout"


class TestResearchSettingsPropagation:
    """Tests for settings propagation through research."""

    def test_settings_available_in_all_phases(self):
        """Settings should be available in all phases."""
        settings = {
            "llm.model": "gpt-4",
            "llm.temperature": 0.7,
            "search.max_results": 10,
        }

        phases_settings = {}

        def run_phase(phase_name, settings):
            phases_settings[phase_name] = dict(settings)
            return settings.get("llm.model")

        # Run phases with settings
        run_phase("analysis", settings)
        run_phase("search", settings)
        run_phase("synthesis", settings)

        # All phases should have same settings
        for phase in ["analysis", "search", "synthesis"]:
            assert phases_settings[phase]["llm.model"] == "gpt-4"

    def test_settings_override_defaults(self):
        """User settings should override defaults."""
        defaults = {
            "llm.temperature": 0.5,
            "search.max_results": 5,
        }
        user_settings = {
            "llm.temperature": 0.8,
        }

        def merge_settings(defaults, user):
            merged = dict(defaults)
            merged.update(user)
            return merged

        merged = merge_settings(defaults, user_settings)

        assert merged["llm.temperature"] == 0.8  # User override
        assert merged["search.max_results"] == 5  # Default kept


class TestResearchSourceDeduplication:
    """Tests for source deduplication."""

    def test_duplicate_urls_removed(self):
        """Duplicate URLs should be removed."""
        sources = [
            {"url": "http://example.com/1", "title": "Source 1"},
            {"url": "http://example.com/2", "title": "Source 2"},
            {"url": "http://example.com/1", "title": "Source 1 Duplicate"},
        ]

        def deduplicate_sources(sources):
            seen_urls = set()
            unique = []
            for source in sources:
                if source["url"] not in seen_urls:
                    seen_urls.add(source["url"])
                    unique.append(source)
            return unique

        unique = deduplicate_sources(sources)

        assert len(unique) == 2

    def test_similar_content_detected(self):
        """Similar content should be detected."""
        sources = [
            {"content": "Machine learning is a branch of AI"},
            {
                "content": "Machine learning is a branch of artificial intelligence"
            },
        ]

        def calculate_similarity(text1, text2):
            # Simple word overlap similarity
            words1 = set(text1.lower().split())
            words2 = set(text2.lower().split())
            overlap = len(words1 & words2)
            total = len(words1 | words2)
            return overlap / total if total > 0 else 0

        similarity = calculate_similarity(
            sources[0]["content"], sources[1]["content"]
        )

        assert similarity > 0.5  # High similarity
