#!/usr/bin/env python3
"""
Backend compatibility tests for xwquery actions.
Validates that all registered actions execute on xwnode and xwdata backends.
"""

from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Any

import pytest

from exonware.xwquery.contracts import ExecutionContext, QueryAction
from exonware.xwquery.runtime.engines import XWNodeOperationsExecutionEngine
from exonware.xwquery.runtime.executors import get_operation_registry
from exonware.xwquery.runtime.executors.engine import NativeOperationsExecutionEngine


def _add_sibling_src_paths() -> None:
    repo_root = Path(__file__).resolve().parents[2]
    for sibling in ("xwnode", "xwdata", "xwsystem", "xwlazy"):
        src = repo_root.parent / sibling / "src"
        if src.exists():
            src_str = str(src)
            if src_str not in sys.path:
                sys.path.insert(0, src_str)


_add_sibling_src_paths()

try:
    import exonware  # type: ignore

    repo_root = Path(__file__).resolve().parents[2]
    for sibling in ("xwnode", "xwdata", "xwsystem", "xwlazy"):
        candidate = repo_root.parent / sibling / "src" / "exonware"
        if candidate.exists():
            candidate_str = str(candidate)
            if candidate_str not in list(exonware.__path__):
                exonware.__path__.append(candidate_str)
except Exception:
    # Keep tests import-safe; importorskip below handles availability.
    pass

from exonware.xwnode import XWNode
from exonware.xwdata import XWData


def _build_params(op: str, io_paths: dict[str, str]) -> dict[str, Any]:
    # Shared robust defaults
    params: dict[str, Any] = {
        "field": "name",
        "as_field": "result",
    }
    specific: dict[str, dict[str, Any]] = {
        # Core / filtering
        "TRUNCATE": {"target": "users"},
        "INSERT": {"target": "users", "values": {"id": 3, "name": "Eve"}},
        "UPSERT": {"target": "users", "data": {"id": 2, "name": "Bob Updated"}, "key_fields": ["id"]},
        "EXPLAIN": {"query": "SELECT * FROM users", "verbose": True},
        "WHERE": {"field": "age", "operator": ">", "value": 20},
        "FILTER": {"field": "age", "operator": ">", "value": 20},
        "LIKE": {"field": "name", "pattern": "Ali%"},
        "IN": {"field": "city", "values": ["Austin", "Boston"]},
        "NOT_IN": {"field": "city", "values": ["Nowhere"]},
        "BETWEEN": {"field": "age", "min": 20, "max": 40},
        "RANGE": {"field": "score", "min": 0, "max": 100},
        "TERM": {"field": "city", "value": "Austin"},
        "EXISTS": {"field": "email", "negate": False},
        "IS_NULL": {"field": "email", "negate": False},
        "IS_MISSING": {"field": "not_present", "negate": False},
        "CONTAINS": {"field": "name", "value": "Alice"},
        "STARTS_WITH": {"field": "name", "prefix": "A"},
        "ENDS_WITH": {"field": "name", "suffix": "th"},
        "REGEX": {"field": "email", "pattern": r".+@.+", "flags": "i"},
        "FUZZY": {"field": "city", "value": "Austin", "max_distance": 2},
        "ANY_MATCH": {"field": "arr", "condition": {"operator": ">", "value": 2}},
        "ALL_MATCH": {"field": "arr", "condition": {"operator": ">", "value": 0}},
        # Aggregation
        "COUNT": {"field": "id"},
        "SUM": {"field": "score"},
        "AVG": {"field": "score"},
        "MIN": {"field": "score"},
        "MAX": {"field": "score"},
        "GROUP": {"field": "city"},
        "HAVING": {"condition": {"field": "count", "operator": ">", "value": 0}},
        "COLLECT": {"field": "city", "unique": True, "as_field": "cities"},
        "REDUCE": {"field": "score", "operation": "sum", "initial": 0, "as_field": "reduced"},
        "STDDEV": {"field": "score"},
        "VARIANCE": {"field": "score"},
        "MEDIAN": {"field": "score"},
        "PERCENTILE": {"field": "score", "percentile": 50},
        "FIRST_VALUE": {"field": "name"},
        "LAST_VALUE": {"field": "name"},
        "GROUP_CONCAT": {"field": "city", "separator": ",", "distinct": True},
        # Projection/order
        "PROJECT": {"fields": ["id", "name"]},
        "EXTEND": {"expressions": {"score2": "score * 2"}},
        "ORDER": {"field": "score", "direction": "desc"},
        "BY": {"field": "score", "direction": "desc"},
        "LIMIT": {"limit": 2},
        "OFFSET": {"offset": 1},
        # Graph
        "MATCH": {"pattern": {"label": "Person"}},
        "PATH": {"algorithm": "shortest"},
        "OUT": {"edge_type": "KNOWS"},
        "IN_TRAVERSE": {"edge_type": "KNOWS"},
        "RETURN": {"fields": ["id", "name"]},
        "SHORTEST_PATH": {"source": 1, "target": 2},
        "ALL_SHORTEST_PATHS": {"source": 1, "target": 2},
        "SIMPLE_PATH": {"source": 1, "target": 2},
        "ALL_SIMPLE_PATHS": {"source": 1, "target": 2},
        "ALL_PATHS": {"source": 1, "target": 2},
        "TRAVERSAL": {"start": 1, "max_depth": 2},
        "CONNECTED_COMPONENTS": {},
        "CYCLE_DETECTION": {},
        "EXPAND": {"depth": 2},
        "NEIGHBORS": {"node_id": 1},
        "DEGREE": {"node_id": 1},
        "CLONE": {"node_id": 1},
        "SUBGRAPH": {"node_ids": [1, 2]},
        "VARIABLE_PATH": {"source": 1, "target": 2, "min_hops": 1, "max_hops": 3},
        "PATH_LENGTH": {"source": 1, "target": 2},
        "EXTRACT_PATH": {"source": 1, "target": 2},
        "PROPERTIES": {"node_id": 1},
        "SET": {"field": "tag", "value": "ok"},
        "CREATE_EDGE": {"from": 1, "to": 2, "label": "LINKS"},
        "DELETE_EDGE": {"from": 1, "to": 2, "label": "LINKS"},
        "UPDATE_EDGE": {"from": 1, "to": 2, "label": "LINKS", "properties": {"weight": 1}},
        "DETACH_DELETE": {"node_id": 1},
        "BOTH": {},
        "BOTHE": {},
        "BOTHV": {},
        "INE": {},
        "INV": {},
        "OUTE": {},
        "OUTV": {},
        "REPEAT": {"times": 2, "emit": True},
        "COALESCE_STEP": {"alternatives": []},
        "CHOOSE": {"condition": {"field": "age", "operator": ">", "value": 25}, "true_branch": [], "false_branch": []},
        "ADD_VERTEX": {"label": "Person", "properties": {"name": "New"}},
        "DROP_VERTEX": {"filter": {"label": "Person"}},
        "PAGE_RANK": {"damping": 0.85, "iterations": 3, "edge_field": "adj", "as_field": "rank"},
        # Data
        "LOAD": {"source": io_paths["import"], "format": "json"},
        "STORE": {"target": io_paths["export"], "format": "json"},
        "FILE_SOURCE": {"source": io_paths["import"], "format": "json"},
        "IMPORT": {"source": io_paths["import"], "format": "json"},
        "EXPORT": {"target": io_paths["export"], "format": "json"},
        "COPY": {"source": "users", "target": "users_copy"},
        "BULK_WRITE": {"operations": [{"type": "insert", "data": {"id": 3, "name": "Eve"}}], "ordered": True},
        # Array
        "SLICING": {"field": "arr", "start": 0, "end": 2},
        "INDEXING": {"field": "arr", "index": 0},
        "UNWIND": {"field": "arr", "preserve_null": True},
        "FLATTEN": {"field": "nested", "depth": -1, "as_field": "flat"},
        "ARRAY_LENGTH": {"field": "arr", "as_field": "arr_len"},
        "ARRAY_CONTAINS": {"field": "arr", "value": 3, "as_field": "arr_has"},
        "ARRAY_SORT": {"field": "arr", "as_field": "arr_sorted"},
        "ARRAY_REVERSE": {"field": "arr", "as_field": "arr_rev"},
        "ARRAY_CONCAT": {"fields": ["arr", "arr2"], "as_field": "arr_cat"},
        "ARRAY_DISTINCT": {"field": "arr", "as_field": "arr_dist"},
        "PUSH": {"field": "arr", "value": 11},
        "POP": {"field": "arr", "position": -1, "as_field": "popped"},
        # Advanced
        "JOIN": {"source": [{"id": 1, "v": 10}], "on": ["id"], "how": "inner"},
        "UNION": {"sources": [[{"id": 99}]], "distinct": True},
        "WITH": {"name": "cte", "query": "SELECT 1"},
        "AGGREGATE": {"type": "SUM", "field": "score"},
        "FOREACH": {"collection": [1, 2, 3]},
        "LET": {"variable": "x", "value": 1},
        "FOR": {"start": 0, "end": 5, "step": 1},
        "WINDOW": {"function": "ROW_NUMBER", "partition_by": ["city"], "order_by": ["score"]},
        "DESCRIBE": {"resource": "users"},
        "CONSTRUCT": {"template": "{?s ?p ?o}", "where": "..."},
        "ASK": {"pattern": {"type": "Person"}},
        "SUBSCRIBE": {"event": "created"},
        "SUBSCRIPTION": {"event": "created"},
        "MUTATION": {"type": "update", "fields": {"tag": "mutated"}},
        "PIPE": {"operations": ["filter", "map"]},
        "OPTIONS": {"options": {"timeout": 1000}},
        "INCLUDE": {"fields": ["id", "name"]},
        "MINUS": {"exclude": [{"id": 999}]},
        "INTERSECT": {"source": [{"id": 1}], "key_fields": ["id"]},
        "SUBQUERY": {"query_actions": [], "as_field": "sub"},
        "PIVOT": {"row_field": "city", "column_field": "name", "value_field": "score", "agg": "sum"},
        "UNPIVOT": {"value_columns": ["age", "score"], "name_field": "metric", "value_field": "value"},
        "ROW_NUMBER": {"partition_by": ["city"], "order_by": {"field": "score", "direction": "desc"}},
        "RANK": {"partition_by": ["city"], "order_by": {"field": "score", "direction": "desc"}},
        "DENSE_RANK": {"partition_by": ["city"], "order_by": {"field": "score", "direction": "desc"}},
        "RECURSIVE_CTE": {"base_items": [{"id": 1, "parent_id": None}], "recursive_field": "parent_id", "id_field": "id", "max_depth": 3},
        # Conditional
        "CASE": {"field": "age", "conditions": [{"when": {"field": "age", "operator": ">=", "value": 30}, "then": "senior"}], "else_value": "junior"},
        "COALESCE": {"fields": ["email", "name"], "default": "N/A", "as_field": "coalesced"},
        "NULLIF": {"field": "city", "compare_value": "Austin", "as_field": "nullified"},
        "IF_EXPR": {"condition": {"field": "age", "operator": ">", "value": 25}, "then_value": "adult", "else_value": "young"},
        "SWITCH": {"field": "city", "branches": [{"case": "Austin", "then": "TX"}], "default": "OTHER"},
        # String
        "CONCAT": {"fields": ["name", "city"], "separator": " | ", "as_field": "concat"},
        "SUBSTRING": {"field": "name", "start": 0, "length": 5, "as_field": "sub"},
        "UPPER": {"field": "city", "as_field": "upper"},
        "LOWER": {"field": "city", "as_field": "lower"},
        "TRIM": {"field": "name", "side": "both", "as_field": "trimmed"},
        "REPLACE_STR": {"field": "name", "old": " ", "new": "_", "as_field": "repl"},
        "SPLIT": {"field": "email", "delimiter": "@", "as_field": "parts"},
        "LENGTH": {"field": "name", "as_field": "len"},
        "LPAD": {"field": "city", "width": 10, "fill_char": ".", "as_field": "lpad"},
        "RPAD": {"field": "city", "width": 10, "fill_char": ".", "as_field": "rpad"},
        "REVERSE_STR": {"field": "city", "as_field": "rev"},
        "POSITION": {"field": "name", "substring": " ", "as_field": "pos"},
        "REPEAT_STR": {"field": "city", "times": 2, "as_field": "rep"},
        "REGEX_EXTRACT": {"field": "email", "pattern": r"^([^@]+)", "group": 1, "as_field": "user"},
        "REGEX_REPLACE": {"field": "name", "pattern": r"\\s+", "replacement": "_", "as_field": "name2"},
        # Math
        "ABS": {"field": "signed", "as_field": "abs"},
        "CEIL": {"field": "score", "as_field": "ceil"},
        "FLOOR": {"field": "score", "as_field": "floor"},
        "ROUND": {"field": "score", "precision": 0, "as_field": "round"},
        "POWER": {"field": "age", "exponent": 2, "as_field": "pow"},
        "SQRT": {"field": "score", "as_field": "sqrt"},
        "MOD": {"field": "age", "divisor": 3, "as_field": "mod"},
        "LOG": {"field": "score", "base": 2, "as_field": "log"},
        "EXP": {"field": "signed", "as_field": "exp"},
        "SIGN": {"field": "signed", "as_field": "sign"},
        "RANDOM": {"min_val": 0, "max_val": 1, "seed": 42, "as_field": "rand"},
        "GREATEST": {"fields": ["age", "score"], "as_field": "greatest"},
        "LEAST": {"fields": ["age", "score"], "as_field": "least"},
        "TRUNC": {"field": "score", "as_field": "trunc"},
        # Date / type
        "NOW": {"as_field": "now", "utc": True},
        "DATE_ADD": {"field": "created", "value": 1, "unit": "days", "as_field": "d1"},
        "DATE_DIFF": {"field1": "created", "field2": "created", "unit": "seconds", "as_field": "dd"},
        "EXTRACT_DATE": {"field": "created", "component": "year", "as_field": "year"},
        "DATE_FORMAT": {"field": "created", "format": "%Y-%m-%d", "as_field": "df"},
        "DATE_PARSE": {"field": "created", "format": "%Y-%m-%d %H:%M:%S", "as_field": "dp"},
        "DATE_TRUNC": {"field": "created", "precision": "day", "as_field": "dt"},
        "INTERVAL": {"value": 3, "unit": "days", "as_field": "iv"},
        "UNIX_TIMESTAMP": {"field": "created", "as_field": "uts"},
        "FROM_UNIXTIME": {"field": "id", "format": "%Y-%m-%d", "as_field": "fut"},
        "CAST": {"field": "age", "target_type": "str", "as_field": "age_s"},
        "TYPEOF": {"field": "age", "as_field": "type"},
        "TRY_CAST": {"field": "city", "target_type": "int", "default": None, "as_field": "maybe_int"},
        "TO_STRING": {"field": "age", "as_field": "to_s"},
        "TO_NUMBER": {"field": "age", "number_type": "float", "as_field": "to_n"},
        "TO_BOOLEAN": {"field": "email", "as_field": "to_b"},
        "TO_DATE": {"field": "created", "format": "%Y-%m-%d %H:%M:%S", "as_field": "to_d"},
        "IS_NUMBER": {"field": "age", "as_field": "is_num"},
        "IS_STRING": {"field": "name", "as_field": "is_str"},
    }
    params.update(specific.get(op, {}))
    return params


@pytest.fixture
def base_rows() -> list[dict[str, Any]]:
    return [
        {
            "id": 1,
            "name": "Alice Smith",
            "city": "Austin",
            "email": "alice@example.com",
            "age": 30,
            "score": 10.5,
            "signed": -3.2,
            "created": "2025-01-02 10:00:00",
            "arr": [3, 1, 3],
            "arr2": [9],
            "nested": [1, [2, [3]]],
            "tags": ["python", "data"],
            "adj": [2],
            "label": "Person",
        },
        {
            "id": 2,
            "name": "Bob Jones",
            "city": "Boston",
            "email": None,
            "age": 24,
            "score": 20.1,
            "signed": 4.7,
            "created": "2025-01-03 15:30:00",
            "arr": [5, 2],
            "arr2": [8, 7],
            "nested": [[4], 5],
            "tags": ["go", "api"],
            "adj": [1],
            "label": "Person",
        },
    ]


@pytest.fixture
def io_paths(tmp_path: Path) -> dict[str, str]:
    import_file = tmp_path / "import.json"
    import_file.write_text(json.dumps([{"id": 101, "name": "Imported"}]), encoding="utf-8")
    export_file = tmp_path / "export.json"
    return {"import": str(import_file), "export": str(export_file)}


def _all_registered_operations() -> list[str]:
    registry = get_operation_registry()
    return sorted(registry.list_operations())


def _requires_linear_or_tree_window(op: str) -> bool:
    return op in {"SLICING", "INDEXING", "WINDOW"}


@pytest.mark.xwquery_integration
def test_all_registered_ops_execute_on_xwnode(base_rows: list[dict[str, Any]], io_paths: dict[str, str]):
    tree_node = XWNode.from_native({"rows": base_rows, "nodes": base_rows})
    linear_node = XWNode.from_native(base_rows)
    engine = XWNodeOperationsExecutionEngine()

    failures: list[str] = []
    for op in _all_registered_operations():
        params = _build_params(op, io_paths)
        action = QueryAction(type=op, params=params)
        target_node = linear_node if _requires_linear_or_tree_window(op) else tree_node
        context = ExecutionContext(node=target_node, metadata={"engine": engine})
        result = engine.execute_tree(action, context)
        if not result.success:
            failures.append(f"{op}: {result.error}")

    assert not failures, "xwnode backend failures:\n" + "\n".join(failures)


@pytest.mark.xwquery_integration
def test_all_registered_ops_execute_on_xwdata(base_rows: list[dict[str, Any]], io_paths: dict[str, str]):
    # Official integration path in xwdata facade uses as_xwnode() before querying.
    tree_data_obj = XWData.from_native({"rows": base_rows, "nodes": base_rows})
    linear_data_obj = XWData.from_native(base_rows)
    tree_node = tree_data_obj.as_xwnode()
    linear_node = linear_data_obj.as_xwnode()
    engine = XWNodeOperationsExecutionEngine()

    failures: list[str] = []
    for op in _all_registered_operations():
        params = _build_params(op, io_paths)
        action = QueryAction(type=op, params=params)
        target_node = linear_node if _requires_linear_or_tree_window(op) else tree_node
        context = ExecutionContext(node=target_node, metadata={"engine": engine})
        result = engine.execute_tree(action, context)
        if not result.success:
            failures.append(f"{op}: {result.error}")

    assert not failures, "xwdata backend failures:\n" + "\n".join(failures)

