#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Final 3 audit item tests:

1. _record_retrievals is async (submitted to executor, not blocking)
2. Graph multi-word LIKE query splits terms
3. Consolidation Stage 1 dedup completes without timeout
"""
import sqlite3
from unittest.mock import patch, MagicMock, ANY

import pytest


# ── Test 1: Async _record_retrievals ────────────────────────────────


class TestAsyncRetrievals:
    """Verify _record_retrievals is fire-and-forget via _EXECUTOR."""

    def test_record_retrievals_submitted_to_executor(self):
        """_record_retrievals must be submitted via _EXECUTOR.submit, not called directly."""
        import ast
        import inspect
        from pulse.brain.brain_search import search_engine

        source = inspect.getsource(search_engine.brain_search)
        tree = ast.parse(source)
        # Find all calls to _EXECUTOR.submit(_record_retrievals, ...)
        found_submit = False
        found_direct = False
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                func = node.func
                # Check for _EXECUTOR.submit(_record_retrievals, ...)
                if (isinstance(func, ast.Attribute) and func.attr == "submit"
                        and isinstance(func.value, ast.Name)
                        and func.value.id == "_EXECUTOR"):
                    if (node.args and isinstance(node.args[0], ast.Name)
                            and node.args[0].id == "_record_retrievals"):
                        found_submit = True
                # Check for direct _record_retrievals(...) call
                if (isinstance(func, ast.Name)
                        and func.id == "_record_retrievals"):
                    found_direct = True
        assert found_submit, "_EXECUTOR.submit(_record_retrievals, ...) not found"
        assert not found_direct, "_record_retrievals() called directly (should use submit)"

    def test_executor_exists_with_workers(self):
        """_EXECUTOR must be a ThreadPoolExecutor with workers."""
        from concurrent.futures import ThreadPoolExecutor
        from pulse.brain.brain_search.search_engine import _EXECUTOR

        assert isinstance(_EXECUTOR, ThreadPoolExecutor)
        assert _EXECUTOR._max_workers >= 4


# ── Test 2: Graph multi-word LIKE ────────────────────────────────────


class TestGraphMultiwordLike:
    """Verify _find_nodes_sql splits multi-word queries into AND-chained LIKE."""

    def _make_db(self):
        """Create an in-memory SQLite with test graph nodes."""
        conn = sqlite3.connect(":memory:")
        conn.row_factory = sqlite3.Row
        conn.execute(
            "CREATE TABLE graph_nodes (id TEXT, type TEXT, text TEXT, confidence REAL)"
        )
        conn.executemany(
            "INSERT INTO graph_nodes VALUES (?, ?, ?, ?)",
            [
                ("n1", "CONCEPT", "machine learning pipeline", 0.8),
                ("n2", "CONCEPT", "machine vision system", 0.7),
                ("n3", "CONCEPT", "learning from failures", 0.9),
                ("n4", "CONCEPT", "database optimization", 0.6),
                ("n5", "CONCEPT", "deep learning architecture", 0.85),
            ],
        )
        conn.commit()
        return conn

    def test_single_word_query(self):
        """Single-word queries should work unchanged."""
        from pulse.graph.knowledge_graph_query import _find_nodes_sql

        conn = self._make_db()
        results = _find_nodes_sql(conn, "machine", limit=10)
        assert len(results) == 2  # n1 + n2
        texts = [r["text"] for r in results]
        assert "machine learning pipeline" in texts
        assert "machine vision system" in texts

    def test_multiword_query_and_chained(self):
        """Multi-word queries must AND-chain: both words required."""
        from pulse.graph.knowledge_graph_query import _find_nodes_sql

        conn = self._make_db()
        results = _find_nodes_sql(conn, "machine learning", limit=10)
        # Only n1 has BOTH "machine" AND "learning"
        assert len(results) == 1
        assert results[0]["text"] == "machine learning pipeline"

    def test_multiword_noncontiguous(self):
        """Words don't need to be contiguous — AND matching, not substring."""
        from pulse.graph.knowledge_graph_query import _find_nodes_sql

        conn = self._make_db()
        results = _find_nodes_sql(conn, "deep architecture", limit=10)
        assert len(results) == 1
        assert results[0]["text"] == "deep learning architecture"

    def test_no_match_falls_back_to_scorer(self):
        """When LIKE returns nothing, fallback scorer should kick in."""
        from pulse.graph.knowledge_graph_query import _find_nodes_sql

        conn = self._make_db()
        results = _find_nodes_sql(conn, "nonexistent term xyz", limit=10)
        # Fallback scorer may or may not find partial matches
        # The important thing is no crash
        assert isinstance(results, list)

    def test_short_words_filtered(self):
        """Words <=2 chars should be filtered to avoid stopword noise."""
        from pulse.graph.knowledge_graph_query import _find_nodes_sql

        conn = self._make_db()
        # "a" and "of" should be filtered, only "database" matters
        results = _find_nodes_sql(conn, "a database of", limit=10)
        assert len(results) == 1
        assert results[0]["text"] == "database optimization"


# ── Test 3: Consolidation Stage 1 dedup ──────────────────────────────


class TestConsolidationDedup:
    """Verify Stage 1 dedup runs without subprocess timeout."""

    @pytest.mark.timeout(300)
    def test_stage_dedup_completes(self):
        """stage_dedup() must return status='ok' with merged count."""
        from pulse.daemons.consolidation.stages.stage_dedup import stage_dedup

        result = stage_dedup(verbose=False)
        assert result["status"] == "ok", f"Dedup failed: {result.get('error')}"
        assert "merged" in result
        assert isinstance(result["merged"], int)
        assert result["merged"] >= 0

    def test_stage_dedup_no_subprocess(self):
        """Verify dedup uses direct import, not subprocess (the old bug)."""
        import inspect
        from pulse.daemons.consolidation.stages import stage_dedup as mod

        source = inspect.getsource(mod)
        assert "import subprocess" not in source, (
            "stage_dedup should use direct import, not subprocess"
        )
        assert "subprocess.run" not in source, (
            "stage_dedup should not shell out via subprocess.run"
        )
