#!/usr/bin/env python3
"""
Gap coverage tests for newly added xwquery operations.
Ensures all added operations are registered and executable.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from exonware.xwquery.contracts import ExecutionContext, QueryAction
from exonware.xwquery.defs import ALL_OPERATIONS
from exonware.xwquery.runtime.executors import get_operation_registry
from exonware.xwquery.strategies.xwquery import XWQueryScriptStrategy


NEW_OPS = [
    # Wave 1
    "CASE",
    "COALESCE",
    "NULLIF",
    "IF_EXPR",
    "SWITCH",
    "TRUNCATE",
    "UPSERT",
    "EXPLAIN",
    "EXISTS",
    "NOT_IN",
    "IS_NULL",
    # Wave 2: string
    "CONCAT",
    "SUBSTRING",
    "UPPER",
    "LOWER",
    "TRIM",
    "REPLACE_STR",
    "SPLIT",
    "LENGTH",
    "LPAD",
    "RPAD",
    "REVERSE_STR",
    "POSITION",
    "REPEAT_STR",
    "REGEX_EXTRACT",
    "REGEX_REPLACE",
    # Wave 2: math
    "ABS",
    "CEIL",
    "FLOOR",
    "ROUND",
    "POWER",
    "SQRT",
    "MOD",
    "LOG",
    "EXP",
    "SIGN",
    "RANDOM",
    "GREATEST",
    "LEAST",
    "TRUNC",
    # Wave 2: type
    "CAST",
    "TYPEOF",
    "TRY_CAST",
    "TO_STRING",
    "TO_NUMBER",
    "TO_BOOLEAN",
    "TO_DATE",
    "IS_NUMBER",
    "IS_STRING",
    # Wave 3: datetime
    "NOW",
    "DATE_ADD",
    "DATE_DIFF",
    "EXTRACT_DATE",
    "DATE_FORMAT",
    "DATE_PARSE",
    "DATE_TRUNC",
    "INTERVAL",
    "UNIX_TIMESTAMP",
    "FROM_UNIXTIME",
    # Wave 3: filtering
    "CONTAINS",
    "STARTS_WITH",
    "ENDS_WITH",
    "REGEX",
    "FUZZY",
    "IS_MISSING",
    "ANY_MATCH",
    "ALL_MATCH",
    # Wave 4: array
    "UNWIND",
    "FLATTEN",
    "ARRAY_LENGTH",
    "ARRAY_CONTAINS",
    "ARRAY_SORT",
    "ARRAY_REVERSE",
    "ARRAY_CONCAT",
    "ARRAY_DISTINCT",
    "PUSH",
    "POP",
    # Wave 4: aggregation
    "COLLECT",
    "REDUCE",
    "STDDEV",
    "VARIANCE",
    "MEDIAN",
    "PERCENTILE",
    "FIRST_VALUE",
    "LAST_VALUE",
    "GROUP_CONCAT",
    # Wave 5: advanced
    "INTERSECT",
    "SUBQUERY",
    "PIVOT",
    "UNPIVOT",
    "ROW_NUMBER",
    "RANK",
    "DENSE_RANK",
    "RECURSIVE_CTE",
    # Wave 5: graph
    "REPEAT",
    "COALESCE_STEP",
    "CHOOSE",
    "ADD_VERTEX",
    "DROP_VERTEX",
    "PAGE_RANK",
    # Wave 5: data
    "IMPORT",
    "EXPORT",
    "COPY",
    "BULK_WRITE",
]


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


@pytest.fixture
def io_files(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 _params_for(op: str, io_files: dict[str, str]) -> dict:
    common = {
        "field": "s",
        "as_field": "result",
    }
    by_op = {
        "CASE": {
            "field": "age",
            "conditions": [{"when": {"field": "age", "operator": ">=", "value": 30}, "then": "senior"}],
            "else_value": "junior",
            "as_field": "bucket",
        },
        "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",
            "as_field": "group",
        },
        "SWITCH": {
            "field": "city",
            "branches": [{"case": "Austin", "then": "TX"}, {"case": "Boston", "then": "MA"}],
            "default": "OTHER",
            "as_field": "state",
        },
        "TRUNCATE": {"target": "users"},
        "UPSERT": {"target": "users", "data": {"id": 2, "name": "Bob Updated"}, "key_fields": ["id"]},
        "EXPLAIN": {"query": "SELECT * FROM users", "verbose": True},
        "EXISTS": {"field": "email", "negate": False},
        "NOT_IN": {"field": "city", "values": ["Austin"]},
        "IS_NULL": {"field": "email", "negate": False},
        "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": "s", "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"},
        "ABS": {"field": "a", "as_field": "abs"},
        "CEIL": {"field": "a", "as_field": "ceil"},
        "FLOOR": {"field": "a", "as_field": "floor"},
        "ROUND": {"field": "b", "precision": 0, "as_field": "round"},
        "POWER": {"field": "a", "exponent": 2, "as_field": "pow"},
        "SQRT": {"field": "b", "as_field": "sqrt"},
        "MOD": {"field": "b", "divisor": 3, "as_field": "mod"},
        "LOG": {"field": "b", "base": 2, "as_field": "log"},
        "EXP": {"field": "a", "as_field": "exp"},
        "SIGN": {"field": "a", "as_field": "sign"},
        "RANDOM": {"min_val": 0, "max_val": 1, "seed": 42, "as_field": "rand"},
        "GREATEST": {"fields": ["a", "b"], "as_field": "greatest"},
        "LEAST": {"fields": ["a", "b"], "as_field": "least"},
        "TRUNC": {"field": "b", "as_field": "trunc"},
        "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"},
        "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"},
        "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},
        "IS_MISSING": {"field": "unknown_field", "negate": False},
        "ANY_MATCH": {"field": "arr", "condition": {"operator": ">", "value": 2}},
        "ALL_MATCH": {"field": "arr", "condition": {"operator": ">", "value": 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", "reverse": False, "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"},
        "COLLECT": {"field": "city", "unique": False, "as_field": "col"},
        "REDUCE": {"field": "score", "operation": "sum", "initial": 0, "as_field": "red"},
        "STDDEV": {"field": "score", "sample": False, "as_field": "sd"},
        "VARIANCE": {"field": "score", "sample": False, "as_field": "var"},
        "MEDIAN": {"field": "score", "as_field": "med"},
        "PERCENTILE": {"field": "score", "percentile": 50, "method": "linear", "as_field": "pct"},
        "FIRST_VALUE": {"field": "name", "as_field": "first"},
        "LAST_VALUE": {"field": "name", "as_field": "last"},
        "GROUP_CONCAT": {"field": "city", "separator": ",", "as_field": "gc"},
        "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": ["a", "b"], "name_field": "metric", "value_field": "value"},
        "ROW_NUMBER": {"partition_by": ["city"], "order_by": {"field": "score", "direction": "desc"}, "as_field": "rn"},
        "RANK": {"partition_by": ["city"], "order_by": {"field": "score", "direction": "desc"}, "as_field": "rnk"},
        "DENSE_RANK": {"partition_by": ["city"], "order_by": {"field": "score", "direction": "desc"}, "as_field": "drnk"},
        "RECURSIVE_CTE": {"base_items": [{"id": 1, "parent_id": None}], "recursive_field": "parent_id", "id_field": "id", "max_depth": 5},
        "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": "X"}},
        "DROP_VERTEX": {"filter": {"label": "Person"}},
        "PAGE_RANK": {"damping": 0.85, "iterations": 5, "edge_field": "adj", "as_field": "rank"},
        "IMPORT": {"source": io_files["import"], "format": "json", "options": {}},
        "EXPORT": {"target": io_files["export"], "format": "json", "options": {}},
        "COPY": {"source": "users", "target": "users_copy"},
        "BULK_WRITE": {"operations": [{"type": "insert", "data": {"id": 3}}], "ordered": True},
    }
    return by_op.get(op, common)


@pytest.mark.xwquery_core
def test_new_ops_alignment_registry_defs_and_strategy():
    registry = get_operation_registry()
    reg_ops = set(registry.list_operations())
    def_ops = set(ALL_OPERATIONS)
    strategy_ops = set(XWQueryScriptStrategy.ACTION_TYPES)

    assert len(ALL_OPERATIONS) == len(set(ALL_OPERATIONS)), "ALL_OPERATIONS must be unique"
    assert def_ops == reg_ops, "defs.py and registry operations must match exactly"
    assert def_ops == strategy_ops, "defs.py and strategy ACTION_TYPES must match exactly"


@pytest.mark.xwquery_core
@pytest.mark.parametrize("operation", NEW_OPS, ids=lambda op: op.lower())
def test_new_ops_registered_and_smoke_executable(operation: str, base_rows: list[dict], io_files: dict[str, str]):
    registry = get_operation_registry()
    executor = registry.get(operation)
    assert executor is not None, f"{operation} should be registered"

    params = _params_for(operation, io_files)
    context = ExecutionContext(node=base_rows, metadata={})
    action = QueryAction(type=operation, params=params)

    result = executor._do_execute(action, context)
    assert result.success is True, f"{operation} should execute successfully"
    assert result.action_type == operation
    assert result.data is not None

