import json

import duckdb
import pyarrow as pa

from n6k_server.pushdown import build_query

SAMPLE = pa.table(
    {
        "id": pa.array([1, 2, 3], type=pa.int64()),
        "name": pa.array(["alice", "bob", "charlie"], type=pa.utf8()),
        "score": pa.array([95.5, 87.3, 91.8], type=pa.float64()),
        "tag": pa.array(["a", None, "b"], type=pa.utf8()),
    }
)


def _query(columns=None, filters=None):
    db = duckdb.connect()
    db.execute("CREATE SCHEMA IF NOT EXISTS db")
    db.register("demo", SAMPLE)
    db.execute("CREATE VIEW db.demo AS SELECT * FROM demo")
    sql, params = build_query("demo", columns, filters)
    return db.execute(sql, params).to_arrow_table()


class TestApplyColumns:
    def test_none_returns_unchanged(self):
        result = _query()
        assert result.column_names == ["id", "name", "score", "tag"]

    def test_empty_string_returns_unchanged(self):
        result = _query(columns="")
        assert result.column_names == ["id", "name", "score", "tag"]

    def test_valid_columns(self):
        result = _query(columns="id,name")
        assert result.column_names == ["id", "name"]
        assert result.num_rows == 3

    def test_nonexistent_columns_ignored(self):
        sql, params = build_query("demo", "id,nonexistent", None)
        db = duckdb.connect()
        db.register("demo", SAMPLE)
        # nonexistent column will cause DuckDB error, but the SQL should
        # only include columns that pass the safe-name regex
        assert '"id"' in sql
        assert '"nonexistent"' in sql  # it passes regex, DuckDB validates

    def test_all_nonexistent_returns_star(self):
        sql, _ = build_query("demo", "123,@bad", None)
        assert "SELECT *" in sql

    def test_whitespace_trimmed(self):
        result = _query(columns=" id , name ")
        assert result.column_names == ["id", "name"]


class TestApplyFilters:
    def test_none_returns_unchanged(self):
        result = _query()
        assert result.num_rows == 3

    def test_empty_string_returns_unchanged(self):
        result = _query(filters="")
        assert result.num_rows == 3

    def test_malformed_json_returns_unchanged(self):
        result = _query(filters="not json")
        assert result.num_rows == 3

    def test_non_list_json_returns_unchanged(self):
        result = _query(filters='{"col":"id"}')
        assert result.num_rows == 3

    def test_equal(self):
        f = json.dumps([{"col": "id", "op": "=", "value": 1}])
        result = _query(filters=f)
        assert result.column("id").to_pylist() == [1]

    def test_not_equal(self):
        f = json.dumps([{"col": "id", "op": "!=", "value": 1}])
        result = _query(filters=f)
        assert result.column("id").to_pylist() == [2, 3]

    def test_greater(self):
        f = json.dumps([{"col": "id", "op": ">", "value": 1}])
        result = _query(filters=f)
        assert result.column("id").to_pylist() == [2, 3]

    def test_greater_equal(self):
        f = json.dumps([{"col": "id", "op": ">=", "value": 2}])
        result = _query(filters=f)
        assert result.column("id").to_pylist() == [2, 3]

    def test_less(self):
        f = json.dumps([{"col": "id", "op": "<", "value": 3}])
        result = _query(filters=f)
        assert result.column("id").to_pylist() == [1, 2]

    def test_less_equal(self):
        f = json.dumps([{"col": "id", "op": "<=", "value": 2}])
        result = _query(filters=f)
        assert result.column("id").to_pylist() == [1, 2]

    def test_in(self):
        f = json.dumps([{"col": "name", "op": "in", "value": ["alice", "bob"]}])
        result = _query(filters=f)
        assert result.column("name").to_pylist() == ["alice", "bob"]

    def test_is_null(self):
        f = json.dumps([{"col": "tag", "op": "is_null"}])
        result = _query(filters=f)
        assert result.column("name").to_pylist() == ["bob"]

    def test_is_not_null(self):
        f = json.dumps([{"col": "tag", "op": "is_not_null"}])
        result = _query(filters=f)
        assert result.column("name").to_pylist() == ["alice", "charlie"]

    def test_multiple_filters_anded(self):
        f = json.dumps(
            [
                {"col": "id", "op": ">", "value": 1},
                {"col": "score", "op": "<", "value": 90.0},
            ]
        )
        result = _query(filters=f)
        assert result.column("name").to_pylist() == ["bob"]

    def test_unknown_op_skipped(self):
        f = json.dumps(
            [
                {"col": "id", "op": "like", "value": "1"},
                {"col": "id", "op": "=", "value": 2},
            ]
        )
        result = _query(filters=f)
        assert result.column("id").to_pylist() == [2]

    def test_missing_column_skipped(self):
        # Column name with invalid chars gets rejected by regex
        f = json.dumps([{"col": "@bad", "op": "=", "value": 1}])
        result = _query(filters=f)
        assert result.num_rows == 3

    def test_in_with_non_list_value_skipped(self):
        f = json.dumps([{"col": "id", "op": "in", "value": "not a list"}])
        result = _query(filters=f)
        assert result.num_rows == 3
