"""SQL inspection tests for is_terminator() — validates generated SQL structure.

These tests do NOT execute SQL (no PySpark needed). They verify that:
1. NOT EXISTS appears in the correct location (frontier/recursive step)
2. The barrier table, id column, type filter, and predicate are correct
3. Barrier does NOT appear in the base case / edge expansion
4. Barrier coexists correctly with edge filter and sink filter

Uses SimpleSQLSchemaProvider pattern from test_procedural_bfs_rendering.py.
"""

from __future__ import annotations

import re

import pytest

from gsql2rsql.common.schema import (
    EdgeSchema,
    EntityProperty,
    NodeSchema,
)
from gsql2rsql.parser.opencypher_parser import OpenCypherParser
from gsql2rsql.planner.logical_plan import LogicalPlan
from gsql2rsql.planner.pass_manager import optimize_plan
from gsql2rsql.renderer.schema_provider import (
    SimpleSQLSchemaProvider,
    SQLTableDescriptor,
)
from gsql2rsql.renderer.sql_renderer import SQLRenderer


# ------------------------------------------------------------------
# Schema setup: Station nodes + LINK edges, with is_hub attribute
# ------------------------------------------------------------------


def _make_schema() -> SimpleSQLSchemaProvider:
    """Create a schema with Station nodes and LINK edges."""
    schema = SimpleSQLSchemaProvider()

    schema.add_node(
        NodeSchema(
            name="Station",
            properties=[
                EntityProperty("node_id", str),
                EntityProperty("is_hub", bool),
            ],
            node_id_property=EntityProperty("node_id", str),
        ),
        SQLTableDescriptor(
            table_name="hub_nodes",
            node_id_columns=["node_id"],
            filter="node_type = 'Station'",
        ),
    )

    schema.add_edge(
        EdgeSchema(
            name="LINK",
            source_node_id="Station",
            sink_node_id="Station",
            source_id_property=EntityProperty("src", str),
            sink_id_property=EntityProperty("dst", str),
            properties=[
                EntityProperty("src", str),
                EntityProperty("dst", str),
                EntityProperty("src_is_hub", bool),
            ],
        ),
        SQLTableDescriptor(
            entity_id="Station@LINK@Station",
            table_name="hub_edges",
            node_id_columns=["src", "dst"],
            filter="relationship_type = 'LINK'",
        ),
    )

    return schema


def _transpile(
    query: str,
    schema: SimpleSQLSchemaProvider,
    *,
    materialization: str = "temp_tables",
    vlp_mode: str = "procedural",
) -> str:
    """Transpile a Cypher query with the given strategy."""
    parser = OpenCypherParser()
    renderer = SQLRenderer(
        db_schema_provider=schema,
        vlp_rendering_mode=vlp_mode,
        materialization_strategy=materialization,
    )
    ast = parser.parse(query)
    plan = LogicalPlan.process_query_tree(ast, schema)
    optimize_plan(plan)
    plan.resolve(original_query=query)
    return renderer.render_plan(plan)


# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------


def _find_lines_containing(sql: str, pattern: str) -> list[str]:
    """Return all lines containing pattern (case-insensitive)."""
    return [
        line for line in sql.splitlines()
        if pattern.lower() in line.lower()
    ]


# ===================================================================
# Tests: Procedural BFS — temp_tables (Databricks strategy)
# ===================================================================

BARRIER_QUERY = """
MATCH path = (a:Station)-[:LINK*1..5]->(b:Station)
WHERE a.node_id = 'N1'
  AND is_terminator(b.is_hub = true)
RETURN DISTINCT b.node_id AS dst
"""

NO_BARRIER_QUERY = """
MATCH path = (a:Station)-[:LINK*1..5]->(b:Station)
WHERE a.node_id = 'N1'
RETURN DISTINCT b.node_id AS dst
"""


class TestIsTerminatorTempTablesSQLStructure:
    """Inspect SQL generated by temp_tables strategy for is_terminator."""

    def setup_method(self) -> None:
        self.schema = _make_schema()

    def _sql(self, query: str = BARRIER_QUERY) -> str:
        return _transpile(query, self.schema, materialization="temp_tables")

    def test_barrier_not_exists_present(self) -> None:
        """is_terminator: NOT EXISTS must appear in the SQL."""
        sql = self._sql()
        print(f"\n=== temp_tables barrier SQL ===\n{sql}")
        assert "NOT EXISTS" in sql, "Barrier NOT EXISTS missing from SQL"

    def test_barrier_references_correct_table_and_alias(self) -> None:
        """NOT EXISTS subquery must reference hub_nodes with 'barrier' alias."""
        sql = self._sql()
        not_exists_lines = _find_lines_containing(sql, "NOT EXISTS")
        assert len(not_exists_lines) >= 1
        ne_text = not_exists_lines[0]
        assert "hub_nodes" in ne_text, f"Should query hub_nodes, got: {ne_text}"
        assert "barrier" in ne_text, f"Should use 'barrier' alias, got: {ne_text}"

    def test_barrier_checks_node_id_column(self) -> None:
        """NOT EXISTS should join barrier.node_id = _next_node."""
        sql = self._sql()
        not_exists_lines = _find_lines_containing(sql, "NOT EXISTS")
        assert len(not_exists_lines) >= 1
        ne_text = not_exists_lines[0]
        assert "barrier.node_id" in ne_text, f"Should check barrier.node_id, got: {ne_text}"

    def test_barrier_includes_predicate(self) -> None:
        """NOT EXISTS should filter on barrier.is_hub."""
        sql = self._sql()
        not_exists_lines = _find_lines_containing(sql, "NOT EXISTS")
        assert len(not_exists_lines) >= 1
        ne_text = not_exists_lines[0]
        assert "barrier.is_hub" in ne_text.lower() or "is_hub" in ne_text, (
            f"Should filter on barrier.is_hub, got: {ne_text}"
        )

    def test_barrier_not_in_edge_expansion(self) -> None:
        """NOT EXISTS should NOT appear in edge expansion (bfs_edges)."""
        sql = self._sql()
        # Find the edge expansion section
        lines = sql.splitlines()
        in_edge_expansion = False
        edge_expansion_lines: list[str] = []
        for line in lines:
            if "bfs_edges" in line and ("CREATE" in line or "INSERT INTO" in line):
                in_edge_expansion = True
            if in_edge_expansion:
                edge_expansion_lines.append(line)
                if line.strip().endswith(";"):
                    in_edge_expansion = False

        edge_section = "\n".join(edge_expansion_lines)
        assert "NOT EXISTS" not in edge_section, (
            f"NOT EXISTS should NOT be in edge expansion:\n{edge_section}"
        )

    def test_barrier_in_frontier_inside_while(self) -> None:
        """NOT EXISTS should appear in frontier creation inside the WHILE loop."""
        sql = self._sql()
        # Split at WHILE to isolate loop body
        assert "WHILE" in sql, "Expected WHILE loop"
        in_while = sql.split("WHILE", 1)[1]

        # Find frontier creation inside the loop — match the specific CREATE
        # that creates bfs_frontier_N (not bfs_edges which JOINs bfs_frontier)
        frontier_match = re.search(
            r"CREATE TEMPORARY TABLE bfs_frontier_\d+ AS\b.*?;",
            in_while,
            re.DOTALL,
        )
        assert frontier_match is not None, "Frontier creation not found in WHILE loop"
        frontier_sql = frontier_match.group(0)
        assert "NOT EXISTS" in frontier_sql, (
            f"NOT EXISTS missing from frontier inside WHILE:\n{frontier_sql}"
        )

    def test_barrier_node_type_filter_included(self) -> None:
        """The NOT EXISTS subquery should include the node type filter."""
        sql = self._sql()
        not_exists_lines = _find_lines_containing(sql, "NOT EXISTS")
        assert len(not_exists_lines) >= 1
        ne_text = not_exists_lines[0]
        assert "node_type" in ne_text, (
            f"NOT EXISTS should include node_type filter, got: {ne_text}"
        )

    def test_barrier_with_sink_filter(self) -> None:
        """When sink filter coexists with barrier, both should be present."""
        query = """
        MATCH path = (a:Station)-[:LINK*1..5]->(b:Station)
        WHERE a.node_id = 'N1'
          AND b.is_hub = true
          AND is_terminator(b.is_hub = true)
        RETURN DISTINCT b.node_id AS dst
        """
        sql = _transpile(query, self.schema, materialization="temp_tables")
        print(f"\n=== barrier + sink SQL ===\n{sql}")
        assert "NOT EXISTS" in sql, "Barrier NOT EXISTS missing"

    def test_barrier_with_edge_filter(self) -> None:
        """When edge filter (ALL) coexists with barrier, both should be present."""
        query = """
        MATCH path = (a:Station)-[:LINK*1..5]->(b:Station)
        WHERE a.node_id = 'N1'
          AND is_terminator(b.is_hub = true)
          AND ALL(r IN relationships(path) WHERE NOT r.src_is_hub)
        RETURN DISTINCT b.node_id AS dst
        """
        sql = _transpile(query, self.schema, materialization="temp_tables")
        print(f"\n=== barrier + edge filter SQL ===\n{sql}")
        assert "NOT EXISTS" in sql, "Barrier NOT EXISTS missing"
        assert "src_is_hub" in sql, "Edge predicate missing from SQL"

    def test_no_barrier_no_not_exists(self) -> None:
        """Without is_terminator, no NOT EXISTS should appear."""
        sql = _transpile(NO_BARRIER_QUERY, self.schema, materialization="temp_tables")
        assert "NOT EXISTS" not in sql, (
            f"NOT EXISTS should NOT appear without is_terminator:\n{sql}"
        )


# ===================================================================
# Tests: Procedural BFS — numbered_views (PySpark strategy)
# ===================================================================


class TestIsTerminatorNumberedViewsSQLStructure:
    """Inspect SQL generated by numbered_views strategy for is_terminator."""

    def setup_method(self) -> None:
        self.schema = _make_schema()

    def _sql(self, query: str = BARRIER_QUERY) -> str:
        return _transpile(query, self.schema, materialization="numbered_views")

    def test_barrier_not_exists_present(self) -> None:
        """NOT EXISTS must appear in EXECUTE IMMEDIATE frontier."""
        sql = self._sql()
        print(f"\n=== numbered_views barrier SQL ===\n{sql}")
        assert "NOT EXISTS" in sql, "Barrier NOT EXISTS missing from SQL"
        assert "hub_nodes" in sql, "hub_nodes table missing"

    def test_escaped_quotes_in_dynamic_sql(self) -> None:
        """Inside EXECUTE IMMEDIATE, string literals should be escaped ('')."""
        sql = self._sql()
        # The barrier predicate is inside dynamic SQL (EXECUTE IMMEDIATE),
        # so node_type = 'Station' becomes node_type = ''Station''
        # Verify NOT EXISTS is present and the structure is valid
        assert "NOT EXISTS" in sql

    def test_no_barrier_no_not_exists(self) -> None:
        """Without is_terminator, no NOT EXISTS in numbered_views."""
        sql = _transpile(NO_BARRIER_QUERY, self.schema, materialization="numbered_views")
        assert "NOT EXISTS" not in sql


# ===================================================================
# Tests: CTE renderer
# ===================================================================


class TestIsTerminatorCTESQLStructure:
    """Inspect SQL generated by CTE renderer for is_terminator."""

    def setup_method(self) -> None:
        self.schema = _make_schema()

    def _sql(self, query: str = BARRIER_QUERY) -> str:
        return _transpile(query, self.schema, vlp_mode="cte")

    def test_barrier_in_recursive_step(self) -> None:
        """NOT EXISTS must be in the recursive step, not the base case."""
        sql = self._sql()
        print(f"\n=== CTE barrier SQL ===\n{sql}")

        assert "NOT EXISTS" in sql, "Barrier NOT EXISTS missing from CTE SQL"

        parts = sql.split("UNION ALL")
        assert len(parts) >= 2, "Expected UNION ALL in CTE"
        base_case = parts[0]
        recursive_case = parts[1]

        assert "NOT EXISTS" not in base_case, (
            f"NOT EXISTS should NOT be in base case:\n{base_case}"
        )
        assert "NOT EXISTS" in recursive_case, (
            f"NOT EXISTS should be in recursive case:\n{recursive_case}"
        )

    def test_barrier_references_end_node(self) -> None:
        """NOT EXISTS should reference p.end_node (the node being expanded FROM)."""
        sql = self._sql()
        not_exists_lines = _find_lines_containing(sql, "NOT EXISTS")
        assert len(not_exists_lines) >= 1
        ne_text = not_exists_lines[0]
        assert "p.end_node" in ne_text, (
            f"NOT EXISTS should check barrier.node_id = p.end_node, got: {ne_text}"
        )

    def test_barrier_with_edge_filter_cte(self) -> None:
        """Edge filter and barrier should coexist in CTE recursive step."""
        query = """
        MATCH path = (a:Station)-[:LINK*1..5]->(b:Station)
        WHERE a.node_id = 'N1'
          AND is_terminator(b.is_hub = true)
          AND ALL(r IN relationships(path) WHERE NOT r.src_is_hub)
        RETURN DISTINCT b.node_id AS dst
        """
        sql = _transpile(query, self.schema, vlp_mode="cte")
        print(f"\n=== CTE barrier + edge filter ===\n{sql}")

        parts = sql.split("UNION ALL")
        recursive_case = parts[-1] if len(parts) >= 2 else sql

        assert "NOT EXISTS" in recursive_case, "Barrier missing from recursive"
        assert "src_is_hub" in recursive_case, "Edge filter missing from recursive"

    def test_no_barrier_no_not_exists(self) -> None:
        """Without is_terminator, no NOT EXISTS in CTE."""
        sql = _transpile(NO_BARRIER_QUERY, self.schema, vlp_mode="cte")
        assert "NOT EXISTS" not in sql


# ===================================================================
# Detailed SQL structure analysis for temp_tables
# ===================================================================


class TestTempTablesDetailedAnalysis:
    """Line-by-line validation of temp_tables SQL with is_terminator."""

    def setup_method(self) -> None:
        self.schema = _make_schema()

    def test_full_sql_structure(self) -> None:
        """Validate the complete SQL structure for temp_tables with barrier."""
        sql = _transpile(BARRIER_QUERY, self.schema, materialization="temp_tables")
        print(f"\n=== FULL temp_tables SQL ===\n{sql}")

        # 1. Must start with BEGIN
        assert "BEGIN" in sql

        # 2. Must have WHILE loop
        assert "WHILE" in sql

        # 3. Edge expansion must NOT have NOT EXISTS
        in_while = sql.split("WHILE", 1)[1]
        # Match specifically the CREATE that creates bfs_edges_N
        edge_matches = re.findall(
            r"CREATE TEMPORARY TABLE bfs_edges_\d+ AS\b.*?;",
            in_while,
            re.DOTALL,
        )
        for edge_sql in edge_matches:
            assert "NOT EXISTS" not in edge_sql, (
                f"NOT EXISTS leaked into edge expansion:\n{edge_sql}"
            )

        # 4. Frontier creation inside WHILE must have NOT EXISTS
        frontier_match = re.search(
            r"CREATE TEMPORARY TABLE bfs_frontier_\d+ AS\b.*?;",
            in_while,
            re.DOTALL,
        )
        assert frontier_match is not None, "Frontier creation not found in WHILE"
        frontier_sql = frontier_match.group(0)
        assert "NOT EXISTS" in frontier_sql, (
            f"NOT EXISTS missing from frontier inside WHILE:\n{frontier_sql}"
        )
        # Verify barrier components
        assert "hub_nodes" in frontier_sql, "Missing hub_nodes table"
        assert "barrier" in frontier_sql, "Missing barrier alias"
        assert "is_hub" in frontier_sql, "Missing is_hub predicate"

        # 5. Result accumulation must NOT have NOT EXISTS
        result_matches = re.findall(
            r"INSERT INTO bfs_result.*?;",
            in_while,
            re.DOTALL | re.IGNORECASE,
        )
        for result_sql in result_matches:
            assert "NOT EXISTS" not in result_sql, (
                f"NOT EXISTS leaked into result accumulation:\n{result_sql}"
            )

    def test_barrier_semantics_correctness(self) -> None:
        """Verify barrier is semantically correct:
        - Barrier nodes ARE reached (edges to them are in bfs_edges)
        - Barrier nodes are NOT in frontier (can't expand from them)
        - Result includes edges TO barrier nodes
        """
        sql = _transpile(BARRIER_QUERY, self.schema, materialization="temp_tables")
        in_while = sql.split("WHILE", 1)[1] if "WHILE" in sql else ""

        # bfs_frontier DOES have barrier filter — barrier nodes excluded from frontier
        frontier_match = re.search(
            r"CREATE TEMPORARY TABLE bfs_frontier_\d+ AS\b.*?;",
            in_while,
            re.DOTALL,
        )
        if frontier_match:
            assert "NOT EXISTS" in frontier_match.group(0), (
                "Frontier must exclude barrier nodes"
            )

        # bfs_result gets ALL edges (including to barrier nodes)
        result_matches = re.findall(
            r"INSERT INTO bfs_result.*?;",
            in_while,
            re.DOTALL | re.IGNORECASE,
        )
        for result_sql in result_matches:
            assert "NOT EXISTS" not in result_sql, (
                "Result must include edges TO barrier nodes"
            )
