import functools
import operator
import os
from collections import OrderedDict
from datetime import date, datetime, time
from operator import methodcaller

import numpy as np
import pandas as pd
import pytest
import toolz

import ibis
import ibis.common.exceptions as com
import ibis.expr.analysis as L
import ibis.expr.api as api
import ibis.expr.datatypes as dt
import ibis.expr.operations as ops
import ibis.expr.rules as rlz
import ibis.expr.types as ir
from ibis import literal
from ibis.common.exceptions import IbisTypeError
from ibis.expr.signature import Argument as Arg
from ibis.tests.util import assert_equal


def test_null():
    expr = ibis.literal(None)
    assert isinstance(expr, ir.NullScalar)
    assert isinstance(expr.op(), ops.NullLiteral)
    assert expr._arg.value is None

    expr2 = ibis.null()
    assert_equal(expr, expr2)

    assert expr is expr2
    assert expr.type() is dt.null
    assert expr2.type() is dt.null


@pytest.mark.xfail(
    raises=AssertionError,
    reason='UTF-8 support in Impala non-existent at the moment?',
)
def test_unicode():
    assert False


@pytest.mark.parametrize(
    ['value', 'expected_type'],
    [
        (5, 'int8'),
        (127, 'int8'),
        (128, 'int16'),
        (32767, 'int16'),
        (32768, 'int32'),
        (2147483647, 'int32'),
        (2147483648, 'int64'),
        (-5, 'int8'),
        (-128, 'int8'),
        (-129, 'int16'),
        (-32769, 'int32'),
        (-2147483649, 'int64'),
        (1.5, 'double'),
        ('foo', 'string'),
        ([1, 2, 3], 'array<int8>'),
    ],
)
def test_literal_with_implicit_type(value, expected_type):
    expr = ibis.literal(value)

    assert isinstance(expr, ir.ScalarExpr)
    assert expr.type() == dt.dtype(expected_type)

    assert isinstance(expr.op(), ops.Literal)
    assert expr.op().value is value


pointA = (1, 2)
pointB = (-3, 4)
pointC = (5, 19)
lineAB = [pointA, pointB]
lineBC = [pointB, pointC]
lineCA = [pointC, pointA]
polygon1 = [lineAB, lineBC, lineCA]
polygon2 = [lineAB, lineBC, lineCA]
multipolygon1 = [polygon1, polygon2]


@pytest.mark.parametrize(
    ['value', 'expected_type'],
    [
        (5, 'int16'),
        (127, 'double'),
        (128, 'int64'),
        (32767, 'double'),
        (32768, 'float'),
        (2147483647, 'int64'),
        (-5, 'int16'),
        (-128, 'int32'),
        (-129, 'int64'),
        (-32769, 'float'),
        (-2147483649, 'double'),
        (1.5, 'double'),
        ('foo', 'string'),
        (list(pointA), 'point'),
        (tuple(pointA), 'point'),
        (list(lineAB), 'linestring'),
        (tuple(lineAB), 'linestring'),
        (list(polygon1), 'polygon'),
        (tuple(polygon1), 'polygon'),
        (list(multipolygon1), 'multipolygon'),
        (tuple(multipolygon1), 'multipolygon'),
    ],
)
def test_literal_with_explicit_type(value, expected_type):
    expr = ibis.literal(value, type=expected_type)
    assert expr.type().equals(dt.validate_type(expected_type))


@pytest.mark.parametrize(
    ['value', 'expected_type', 'expected_class'],
    [
        (list('abc'), 'array<string>', ir.ArrayScalar),
        ([1, 2, 3], 'array<int8>', ir.ArrayScalar),
        ({'a': 1, 'b': 2, 'c': 3}, 'map<string, int8>', ir.MapScalar),
        ({1: 2, 3: 4, 5: 6}, 'map<int8, int8>', ir.MapScalar),
        (
            {'a': [1.0, 2.0], 'b': [], 'c': [3.0]},
            'map<string, array<double>>',
            ir.MapScalar,
        ),
        (
            OrderedDict(
                [
                    ('a', 1),
                    ('b', list('abc')),
                    ('c', OrderedDict([('foo', [1.0, 2.0])])),
                ]
            ),
            'struct<a: int8, b: array<string>, c: struct<foo: array<double>>>',
            ir.StructScalar,
        ),
    ],
)
def test_literal_complex_types(value, expected_type, expected_class):
    expr = ibis.literal(value)
    expr_type = expr.type()
    assert expr_type.equals(dt.validate_type(expected_type))
    assert isinstance(expr, expected_class)
    assert isinstance(expr.op(), ops.Literal)
    assert expr.op().value is value


def test_struct_operations():
    value = OrderedDict(
        [
            ('a', 1),
            ('b', list('abc')),
            ('c', OrderedDict([('foo', [1.0, 2.0])])),
        ]
    )
    expr = ibis.literal(value)
    assert isinstance(expr, ir.StructValue)
    assert isinstance(expr.b, ir.ArrayValue)
    assert isinstance(expr.a.op(), ops.StructField)


def test_simple_map_operations():
    value = {'a': [1.0, 2.0], 'b': [], 'c': [3.0]}
    value2 = {'a': [1.0, 2.0], 'c': [3.0], 'd': [4.0, 5.0]}
    expr = ibis.literal(value)
    expr2 = ibis.literal(value2)
    assert isinstance(expr, ir.MapValue)
    assert isinstance(expr.length().op(), ops.MapLength)
    assert isinstance((expr + expr2).op(), ops.MapConcat)
    assert isinstance((expr2 + expr).op(), ops.MapConcat)

    default = ibis.literal([0.0])
    assert isinstance(expr.get('d', default).op(), ops.MapValueOrDefaultForKey)

    # test for an invalid default type, nulls are ok
    with pytest.raises(IbisTypeError):
        expr.get('d', ibis.literal('foo'))

    assert isinstance(
        expr.get('d', ibis.literal(None)).op(), ops.MapValueOrDefaultForKey
    )

    assert isinstance(expr['b'].op(), ops.MapValueForKey)
    assert isinstance(expr.keys().op(), ops.MapKeys)
    assert isinstance(expr.values().op(), ops.MapValues)


@pytest.mark.parametrize(
    ['value', 'expected_type'],
    [
        (32767, 'int8'),
        (32768, 'int16'),
        (2147483647, 'int16'),
        (2147483648, 'int32'),
        ('foo', 'double'),
    ],
)
def test_literal_with_non_coercible_type(value, expected_type):
    expected_msg = 'Value .* cannot be safely coerced to .*'
    with pytest.raises(TypeError, match=expected_msg):
        ibis.literal(value, type=expected_type)


def test_non_inferrable_literal():
    expected_msg = (
        'The datatype of value .* cannot be inferred, try '
        'passing it explicitly with the `type` keyword.'
    )

    value = tuple(pointA)

    with pytest.raises(TypeError, match=expected_msg):
        ibis.literal(value)

    point = ibis.literal(value, type='point')
    assert point.type() == dt.point


def test_literal_list():
    what = [1, 2, 1000]
    expr = api.literal(what)

    assert isinstance(expr, ir.ArrayScalar)

    # it works!
    repr(expr)


def test_literal_array():
    what = []
    expr = api.literal(what)
    assert isinstance(expr, ir.ArrayValue)
    assert expr.type().equals(dt.Array(dt.null))


def test_mixed_arity(table):
    what = ["bar", table.g, "foo"]
    expr = api.as_value_expr(what)

    values = expr.op().values
    assert isinstance(values[1], ir.StringColumn)

    # it works!
    repr(expr)


@pytest.mark.parametrize('container', [list, tuple, set, frozenset])
def test_isin_notin_list(table, container):
    values = container([1, 2, 3, 4])

    expr = table.a.isin(values)
    not_expr = table.a.notin(values)

    assert isinstance(expr, ir.BooleanColumn)
    assert isinstance(expr.op(), ops.Contains)

    assert isinstance(not_expr, ir.BooleanColumn)
    assert isinstance(not_expr.op(), ops.NotContains)


def test_value_counts(table, string_col):
    bool_clause = table[string_col].notin(['1', '4', '7'])
    expr = table[bool_clause][string_col].value_counts()
    assert isinstance(expr, ir.TableExpr)


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_isin_not_comparable():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_isin_array_expr():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_isin_invalid_cases():
    # For example, array expression in a list of values, where the inner
    # array values originate from some other table
    assert False


def test_isin_notin_scalars():
    a, b, c = [ibis.literal(x) for x in [1, 1, 2]]

    result = a.isin([1, 2])
    assert isinstance(result, ir.BooleanScalar)

    result = a.notin([b, c, 3])
    assert isinstance(result, ir.BooleanScalar)


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_isin_null():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_negate_isin():
    # Should yield a NotContains
    assert False


def test_scalar_isin_list_with_array(table):
    val = ibis.literal(2)

    options = [table.a, table.b, table.c]

    expr = val.isin(options)
    assert isinstance(expr, ir.BooleanColumn)

    not_expr = val.notin(options)
    assert isinstance(not_expr, ir.BooleanColumn)


def test_distinct_basic(functional_alltypes):
    expr = functional_alltypes.distinct()
    assert isinstance(expr.op(), ops.Distinct)
    assert isinstance(expr, ir.TableExpr)
    assert expr.op().table is functional_alltypes

    expr = functional_alltypes.string_col.distinct()
    assert isinstance(expr.op(), ops.DistinctColumn)

    assert isinstance(expr, ir.StringColumn)


@pytest.mark.xfail(reason='NYT')
def test_distinct_array_interactions(functional_alltypes):
    # array cardinalities / shapes are likely to be different.
    a = functional_alltypes.int_col.distinct()
    b = functional_alltypes.bigint_col

    with pytest.raises(ir.RelationError):
        a + b


@pytest.mark.parametrize('where', [lambda t: None, lambda t: t.int_col != 0])
def test_distinct_count(functional_alltypes, where):
    result = functional_alltypes.string_col.distinct().count(
        where=where(functional_alltypes)
    )
    assert isinstance(result.op(), ops.CountDistinct)

    expected = functional_alltypes.string_col.nunique(
        where=where(functional_alltypes)
    ).name('count')
    assert result.equals(expected)


def test_distinct_unnamed_array_expr():
    table = ibis.table(
        [('year', 'int32'), ('month', 'int32'), ('day', 'int32')], 'foo'
    )

    # it works!
    expr = (
        ibis.literal('-')
        .join(
            [
                table.year.cast('string'),
                table.month.cast('string'),
                table.day.cast('string'),
            ]
        )
        .distinct()
    )
    repr(expr)


def test_distinct_count_numeric_types(functional_alltypes):
    metric = (
        functional_alltypes.bigint_col.distinct()
        .count()
        .name('unique_bigints')
    )
    functional_alltypes.group_by('string_col').aggregate(metric)


def test_nunique(functional_alltypes):
    expr = functional_alltypes.string_col.nunique()
    assert isinstance(expr.op(), ops.CountDistinct)


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_project_with_distinct():
    assert False


def test_isnull(table):
    expr = table['g'].isnull()
    assert isinstance(expr, ir.BooleanColumn)
    assert isinstance(expr.op(), ops.IsNull)

    expr = ibis.literal('foo').isnull()
    assert isinstance(expr, ir.BooleanScalar)
    assert isinstance(expr.op(), ops.IsNull)


def test_notnull(table):
    expr = table['g'].notnull()
    assert isinstance(expr, ir.BooleanColumn)
    assert isinstance(expr.op(), ops.NotNull)

    expr = ibis.literal('foo').notnull()
    assert isinstance(expr, ir.BooleanScalar)
    assert isinstance(expr.op(), ops.NotNull)


@pytest.mark.parametrize('column', ['e', 'f'], ids=['float', 'double'])
def test_isnan_isinf_column(table, column):
    expr = table[column].isnan()
    assert isinstance(expr, ir.BooleanColumn)
    assert isinstance(expr.op(), ops.IsNan)

    expr = table[column].isinf()
    assert isinstance(expr, ir.BooleanColumn)
    assert isinstance(expr.op(), ops.IsInf)


@pytest.mark.parametrize('value', [1.3, np.nan, np.inf, -np.inf])
def test_isnan_isinf_scalar(value):
    expr = ibis.literal(value).isnan()
    assert isinstance(expr, ir.BooleanScalar)
    assert isinstance(expr.op(), ops.IsNan)

    expr = ibis.literal(value).isinf()
    assert isinstance(expr, ir.BooleanScalar)
    assert isinstance(expr.op(), ops.IsInf)


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_null_literal():
    assert False


@pytest.mark.parametrize(
    ['column', 'operation'],
    [
        ('d', 'cumsum'),
        ('d', 'cummean'),
        ('d', 'cummin'),
        ('d', 'cummax'),
        ('h', 'cumany'),
        ('h', 'cumall'),
    ],
)
def test_cumulative_yield_array_types(table, column, operation):
    expr = getattr(getattr(table, column), operation)()
    assert isinstance(expr, ir.ColumnExpr)


@pytest.fixture(params=['ln', 'log', 'log2', 'log10'])
def log(request):
    return operator.methodcaller(request.param)


@pytest.mark.parametrize('column', list('abcdef'))
def test_log(table, log, column):
    result = log(table[column])
    assert isinstance(result, ir.FloatingColumn)

    # is this what we want?
    # assert result.get_name() == c


def test_log_string(table):
    g = table.g

    with pytest.raises(IbisTypeError):
        ops.Log(g, None).to_expr()


@pytest.mark.parametrize('klass', [ops.Ln, ops.Log2, ops.Log10])
def test_log_variants_string(table, klass):
    g = table.g

    with pytest.raises(IbisTypeError):
        klass(g).to_expr()


def test_log_boolean(table, log):
    # boolean not implemented for these
    h = table['h']
    with pytest.raises(IbisTypeError):
        log(h)


def test_log_literal(log):
    assert isinstance(log(ibis.literal(5)), ir.FloatingScalar)
    assert isinstance(log(ibis.literal(5.5)), ir.FloatingScalar)


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_exp():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_sqrt():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_trig_functions():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_round():
    assert False


def test_cast_same_type_noop(table):
    c = table.g
    assert c.cast('string') is c

    i = ibis.literal(5)
    assert i.cast('int8') is i


@pytest.mark.parametrize('type', ['int8', 'int32', 'double', 'float'])
def test_string_to_number(table, type):
    casted = table.g.cast(type)
    casted_literal = ibis.literal('5').cast(type).name('bar')

    assert isinstance(casted, ir.ColumnExpr)
    assert casted.type() == dt.dtype(type)

    assert isinstance(casted_literal, ir.ScalarExpr)
    assert casted_literal.type() == dt.dtype(type)
    assert casted_literal.get_name() == 'bar'


@pytest.mark.parametrize('col', list('abcdefh'))
def test_number_to_string_column(table, col):
    casted = table[col].cast('string')
    assert isinstance(casted, ir.StringColumn)


def test_number_to_string_scalar():
    casted_literal = ibis.literal(5).cast('string').name('bar')
    assert isinstance(casted_literal, ir.StringScalar)
    assert casted_literal.get_name() == 'bar'


def test_casted_exprs_are_named(table):
    expr = table.f.cast('string')
    assert expr.get_name() == 'cast(f, string)'

    # it works! per GH #396
    expr.value_counts()


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_nonzero():
    assert False


@pytest.mark.parametrize('col', list('abcdefh'))
def test_negate(table, col):
    c = table[col]
    result = -c
    assert isinstance(result, type(c))
    assert isinstance(result.op(), ops.Negate)


def test_negate_boolean_scalar():
    result = -ibis.literal(False)
    assert isinstance(result, ir.BooleanScalar)
    assert isinstance(result.op(), ops.Negate)


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_isnull_notnull():
    assert False


@pytest.mark.parametrize('column', ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
@pytest.mark.parametrize('how', [None, 'first', 'last', 'heavy'])
@pytest.mark.parametrize('condition_fn', [lambda t: None, lambda t: t.a > 8])
def test_arbitrary(table, column, how, condition_fn):
    col = table[column]
    where = condition_fn(table)
    expr = col.arbitrary(how=how, where=where)
    assert expr.type() == col.type()
    assert isinstance(expr, ir.ScalarExpr)
    assert L.is_reduction(expr)


@pytest.mark.parametrize(
    ['column', 'operation'],
    [
        ('h', lambda column: column.any()),
        ('h', lambda column: column.notany()),
        ('h', lambda column: column.all()),
        ('c', lambda column: (column == 0).any()),
        ('c', lambda column: (column == 0).all()),
    ],
)
def test_any_all_notany(table, column, operation):
    expr = operation(table[column])
    assert isinstance(expr, ir.BooleanScalar)
    assert L.is_reduction(expr)


@pytest.mark.parametrize(
    'operation',
    [
        operator.lt,
        operator.gt,
        operator.ge,
        operator.le,
        operator.eq,
        operator.ne,
    ],
)
@pytest.mark.parametrize('column', list('abcdef'))
@pytest.mark.parametrize('case', [2, 2 ** 9, 2 ** 17, 2 ** 33, 1.5])
def test_numbers_compare_numeric_literal(table, operation, column, case):
    ex_op_class = {
        operator.eq: ops.Equals,
        operator.ne: ops.NotEquals,
        operator.le: ops.LessEqual,
        operator.lt: ops.Less,
        operator.ge: ops.GreaterEqual,
        operator.gt: ops.Greater,
    }

    col = table[column]

    result = operation(col, case)
    assert isinstance(result, ir.BooleanColumn)
    assert isinstance(result.op(), ex_op_class[operation])


def test_boolean_comparisons(table):
    bool_col = table.h

    result = bool_col == True  # noqa
    assert isinstance(result, ir.BooleanColumn)

    result = bool_col == False  # noqa
    assert isinstance(result, ir.BooleanColumn)


@pytest.mark.parametrize(
    'operation',
    [
        operator.lt,
        operator.gt,
        operator.ge,
        operator.le,
        operator.eq,
        operator.ne,
    ],
)
def test_string_comparisons(table, operation):
    string_col = table.g
    result = operation(string_col, 'foo')
    assert isinstance(result, ir.BooleanColumn)


@pytest.mark.parametrize(
    'operation', [operator.xor, operator.or_, operator.and_]
)
def test_boolean_logical_ops(table, operation):
    expr = table.a > 0

    result = operation(expr, table.h)
    assert isinstance(result, ir.BooleanColumn)

    result = operation(expr, True)
    refl_result = operation(True, expr)
    assert isinstance(result, ir.BooleanColumn)
    assert isinstance(refl_result, ir.BooleanColumn)

    true = ibis.literal(True)
    false = ibis.literal(False)

    result = operation(true, false)
    assert isinstance(result, ir.BooleanScalar)


def test_null_column():
    t = ibis.table([('a', 'string')], name='t')
    s = t.mutate(b=ibis.NA)
    assert s.b.type() == dt.null
    assert isinstance(s.b, ir.NullColumn)


def test_null_column_union():
    s = ibis.table([('a', 'string'), ('b', 'double')])
    t = ibis.table([('a', 'string')])
    with pytest.raises(ibis.common.exceptions.RelationError):
        s.union(t.mutate(b=ibis.NA))  # needs a type
    assert s.union(t.mutate(b=ibis.NA.cast('double'))).schema() == s.schema()


def test_string_compare_numeric_array(table):
    with pytest.raises(TypeError):
        table.g == table.f

    with pytest.raises(TypeError):
        table.g == table.c


def test_string_compare_numeric_literal(table):
    with pytest.raises(TypeError):
        table.g == ibis.literal(1.5)

    with pytest.raises(TypeError):
        table.g == ibis.literal(5)


def test_between(table):
    result = table.f.between(0, 1)

    assert isinstance(result, ir.BooleanColumn)
    assert isinstance(result.op(), ops.Between)

    # it works!
    result = table.g.between('a', 'f')
    assert isinstance(result, ir.BooleanColumn)

    result = ibis.literal(1).between(table.a, table.c)
    assert isinstance(result, ir.BooleanColumn)

    result = ibis.literal(7).between(5, 10)
    assert isinstance(result, ir.BooleanScalar)

    # Cases where between should immediately fail, e.g. incomparables
    with pytest.raises(TypeError):
        table.f.between('0', '1')

    with pytest.raises(TypeError):
        table.f.between(0, '1')

    with pytest.raises(TypeError):
        table.f.between('0', 1)


def test_chained_comparisons_not_allowed(table):
    with pytest.raises(ValueError):
        0 < table.f < 1


@pytest.mark.parametrize(
    'operation', [operator.add, operator.mul, operator.truediv, operator.sub]
)
def test_binop_string_type_error(table, operation):
    # Strings are not valid for any numeric arithmetic
    ints = table.d
    strs = table.g

    with pytest.raises(TypeError):
        operation(ints, strs)

    with pytest.raises(TypeError):
        operation(strs, ints)


@pytest.mark.parametrize(
    ['op', 'name', 'case', 'ex_type'],
    [
        (operator.add, 'a', 0, 'int8'),
        (operator.add, 'a', 5, 'int16'),
        (operator.add, 'a', 100000, 'int32'),
        (operator.add, 'a', -100000, 'int32'),
        (operator.add, 'a', 1.5, 'double'),
        (operator.add, 'b', 0, 'int16'),
        (operator.add, 'b', 5, 'int32'),
        (operator.add, 'b', -5, 'int32'),
        (operator.add, 'c', 0, 'int32'),
        (operator.add, 'c', 5, 'int64'),
        (operator.add, 'c', -5, 'int64'),
        # technically this can overflow, but we allow it
        (operator.add, 'd', 5, 'int64'),
        (operator.mul, 'a', 0, 'int8'),
        (operator.mul, 'a', 5, 'int16'),
        (operator.mul, 'a', 2 ** 24, 'int32'),
        (operator.mul, 'a', -2 ** 24 + 1, 'int32'),
        (operator.mul, 'a', 1.5, 'double'),
        (operator.mul, 'b', 0, 'int16'),
        (operator.mul, 'b', 5, 'int32'),
        (operator.mul, 'b', -5, 'int32'),
        (operator.mul, 'c', 0, 'int32'),
        (operator.mul, 'c', 5, 'int64'),
        (operator.mul, 'c', -5, 'int64'),
        # technically this can overflow, but we allow it
        (operator.mul, 'd', 5, 'int64'),
        (operator.sub, 'a', 5, 'int16'),
        (operator.sub, 'a', 100000, 'int32'),
        (operator.sub, 'a', -100000, 'int32'),
        (operator.sub, 'a', 1.5, 'double'),
        (operator.sub, 'b', 5, 'int32'),
        (operator.sub, 'b', -5, 'int32'),
        (operator.sub, 'c', 5, 'int64'),
        (operator.sub, 'c', -5, 'int64'),
        # technically this can overflow, but we allow it
        (operator.sub, 'd', 5, 'int64'),
        (operator.truediv, 'a', 5, 'double'),
        (operator.truediv, 'a', 1.5, 'double'),
        (operator.truediv, 'b', 5, 'double'),
        (operator.truediv, 'b', -5, 'double'),
        (operator.truediv, 'c', 5, 'double'),
        (operator.pow, 'a', 0, 'double'),
        (operator.pow, 'b', 0, 'double'),
        (operator.pow, 'c', 0, 'double'),
        (operator.pow, 'd', 0, 'double'),
        (operator.pow, 'e', 0, 'float'),
        (operator.pow, 'f', 0, 'double'),
        (operator.pow, 'a', 2, 'double'),
        (operator.pow, 'b', 2, 'double'),
        (operator.pow, 'c', 2, 'double'),
        (operator.pow, 'd', 2, 'double'),
        (operator.pow, 'a', 1.5, 'double'),
        (operator.pow, 'b', 1.5, 'double'),
        (operator.pow, 'c', 1.5, 'double'),
        (operator.pow, 'd', 1.5, 'double'),
        (operator.pow, 'e', 2, 'float'),
        (operator.pow, 'f', 2, 'double'),
        (operator.pow, 'a', -2, 'double'),
        (operator.pow, 'b', -2, 'double'),
        (operator.pow, 'c', -2, 'double'),
        (operator.pow, 'd', -2, 'double'),
    ],
    ids=lambda arg: str(getattr(arg, '__name__', arg)),
)
def test_literal_promotions(table, op, name, case, ex_type):
    col = table[name]

    result = op(col, case)
    assert result.type() == dt.dtype(ex_type)

    result = op(case, col)
    assert result.type() == dt.dtype(ex_type)


@pytest.mark.parametrize(
    ('op', 'left_fn', 'right_fn', 'ex_type'),
    [
        (operator.sub, lambda t: t['a'], lambda t: 0, 'int8'),
        (operator.sub, lambda t: 0, lambda t: t['a'], 'int16'),
        (operator.sub, lambda t: t['b'], lambda t: 0, 'int16'),
        (operator.sub, lambda t: 0, lambda t: t['b'], 'int32'),
        (operator.sub, lambda t: t['c'], lambda t: 0, 'int32'),
        (operator.sub, lambda t: 0, lambda t: t['c'], 'int64'),
    ],
    ids=lambda arg: str(getattr(arg, '__name__', arg)),
)
def test_zero_subtract_literal_promotions(
    table, op, left_fn, right_fn, ex_type
):
    # in case of zero subtract the order of operands matters
    left, right = left_fn(table), right_fn(table)
    result = op(left, right)

    assert result.type() == dt.dtype(ex_type)


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_add_array_promotions():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_subtract_array_promotions():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_multiply_array_promotions():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_divide_array_promotions():
    assert False


@pytest.mark.xfail(raises=AssertionError, reason='NYT')
def test_string_add_concat():
    assert False


@pytest.fixture
def expr():
    exprs = [ibis.literal(1).name('a'), ibis.literal(2).name('b')]

    return ibis.expr_list(exprs)


def test_names(expr):
    assert expr.names() == ['a', 'b']


def test_prefix(expr):
    prefixed = expr.prefix('foo_')
    result = prefixed.names()
    assert result == ['foo_a', 'foo_b']


def test_rename(expr):
    renamed = expr.rename(lambda x: 'foo({0})'.format(x))
    result = renamed.names()
    assert result == ['foo(a)', 'foo(b)']


def test_suffix(expr):
    suffixed = expr.suffix('.x')
    result = suffixed.names()
    assert result == ['a.x', 'b.x']


def test_concat():
    exprs = [ibis.literal(1).name('a'), ibis.literal(2).name('b')]
    exprs2 = [ibis.literal(3).name('c'), ibis.literal(4).name('d')]

    list1 = ibis.expr_list(exprs)
    list2 = ibis.expr_list(exprs2)

    result = list1.concat(list2)
    expected = ibis.expr_list(exprs + exprs2)
    assert_equal(result, expected)


def test_substitute_dict():
    table = ibis.table([('foo', 'string'), ('bar', 'string')], 't1')
    subs = {'a': 'one', 'b': table.bar}

    result = table.foo.substitute(subs)
    expected = (
        table.foo.case()
        .when('a', 'one')
        .when('b', table.bar)
        .else_(table.foo)
        .end()
    )
    assert_equal(result, expected)

    result = table.foo.substitute(subs, else_=ibis.NA)
    expected = (
        table.foo.case()
        .when('a', 'one')
        .when('b', table.bar)
        .else_(ibis.NA)
        .end()
    )
    assert_equal(result, expected)


@pytest.mark.parametrize(
    'typ',
    [
        'array<map<string, array<array<double>>>>',
        'string',
        'double',
        'float',
        'int64',
    ],
)
def test_not_without_boolean(typ):
    t = ibis.table([('a', typ)], name='t')
    c = t.a
    with pytest.raises(TypeError):
        ~c


@pytest.mark.parametrize(
    ('position', 'names'),
    [
        (0, 'foo'),
        (1, 'bar'),
        ([0], ['foo']),
        ([1], ['bar']),
        ([0, 1], ['foo', 'bar']),
        ([1, 0], ['bar', 'foo']),
    ],
)
@pytest.mark.parametrize(
    'expr_func',
    [
        lambda t, args: t[args],
        lambda t, args: t.sort_by(args),
        lambda t, args: t.group_by(args).aggregate(bar_avg=t.bar.mean()),
    ],
)
def test_table_operations_with_integer_column(position, names, expr_func):
    t = ibis.table([('foo', 'string'), ('bar', 'double')])
    result = expr_func(t, position)
    expected = expr_func(t, names)
    assert result.equals(expected)


@pytest.mark.parametrize('value', ['abcdefg', ['a', 'b', 'c'], [1, 2, 3]])
@pytest.mark.parametrize(
    'operation', ['pow', 'sub', 'truediv', 'floordiv', 'mod']
)
def test_generic_value_api_no_arithmetic(value, operation):
    func = getattr(operator, operation)
    expr = ibis.literal(value)
    with pytest.raises(TypeError):
        func(expr, expr)


@pytest.mark.parametrize(
    ('value', 'expected'), [(5, dt.int8), (5.4, dt.double), ('abc', dt.string)]
)
def test_fillna_null(value, expected):
    assert ibis.NA.fillna(value).type().equals(expected)


@pytest.mark.parametrize(
    ('left', 'right'),
    [
        (literal('2017-04-01'), date(2017, 4, 2)),
        (date(2017, 4, 2), literal('2017-04-01')),
        (literal('2017-04-01 01:02:33'), datetime(2017, 4, 1, 1, 3, 34)),
        (datetime(2017, 4, 1, 1, 3, 34), literal('2017-04-01 01:02:33')),
    ],
)
@pytest.mark.parametrize(
    'op',
    [
        operator.eq,
        operator.ne,
        operator.lt,
        operator.le,
        operator.gt,
        operator.ge,
        lambda left, right: ibis.timestamp('2017-04-01 00:02:34').between(
            left, right
        ),
        lambda left, right: ibis.timestamp('2017-04-01')
        .cast(dt.date)
        .between(left, right),
    ],
)
def test_string_temporal_compare(op, left, right):
    result = op(left, right)
    assert result.type().equals(dt.boolean)


@pytest.mark.parametrize(
    ('value', 'type', 'expected_type_class'),
    [
        (2.21, 'decimal', dt.Decimal),
        (3.14, 'double', dt.Double),
        (4.2, 'int64', dt.Double),
        (4, 'int64', dt.Int64),
    ],
)
def test_decimal_modulo_output_type(value, type, expected_type_class):
    t = ibis.table([('a', type)])
    expr = t.a % value
    assert isinstance(expr.type(), expected_type_class)


@pytest.mark.parametrize(
    ('left', 'right'),
    [(literal('10:00'), time(10, 0)), (time(10, 0), literal('10:00'))],
)
@pytest.mark.parametrize(
    'op',
    [
        operator.eq,
        operator.ne,
        operator.lt,
        operator.le,
        operator.gt,
        operator.ge,
    ],
)
def test_time_compare(op, left, right):
    result = op(left, right)
    assert result.type().equals(dt.boolean)


@pytest.mark.parametrize(
    ('left', 'right'),
    [
        (literal('10:00'), date(2017, 4, 2)),
        (literal('10:00'), datetime(2017, 4, 2, 1, 1)),
        (literal('10:00'), literal('2017-04-01')),
    ],
)
@pytest.mark.parametrize(
    'op', [operator.eq, operator.lt, operator.le, operator.gt, operator.ge]
)
def test_time_timestamp_invalid_compare(op, left, right):
    result = op(left, right)
    assert result.type().equals(dt.boolean)


def test_scalar_parameter_set():
    value = ibis.param({dt.int64})

    assert isinstance(value.op(), ops.ScalarParameter)
    assert value.type().equals(dt.Set(dt.int64))


def test_scalar_parameter_repr():
    value = ibis.param(dt.timestamp).name('value')
    assert repr(value) == 'value = ScalarParameter[timestamp]'

    value_op = value.op()
    assert repr(value_op) == "ScalarParameter(type=timestamp)"


@pytest.mark.parametrize(
    ('left', 'right', 'expected'),
    [
        (
            # same value type, same name
            ibis.param(dt.timestamp),
            ibis.param(dt.timestamp),
            False,
        ),
        (
            # different value type, same name
            ibis.param(dt.date),
            ibis.param(dt.timestamp),
            False,
        ),
        (
            # same value type, different name
            ibis.param(dt.timestamp),
            ibis.param(dt.timestamp),
            False,
        ),
        (
            # different value type, different name
            ibis.param(dt.date),
            ibis.param(dt.timestamp),
            False,
        ),
        (
            # different Python class, left side is param
            ibis.param(dt.timestamp),
            dt.date,
            False,
        ),
        (
            # different Python class, right side is param
            dt.date,
            ibis.param(dt.timestamp),
            False,
        ),
    ],
)
def test_scalar_parameter_compare(left, right, expected):
    assert left.equals(right) == expected


@pytest.mark.parametrize(
    ('case', 'creator'),
    [
        (datetime.now(), toolz.compose(methodcaller('time'), ibis.timestamp)),
        ('now', toolz.compose(methodcaller('time'), ibis.timestamp)),
        (datetime.now().time(), ibis.time),
        ('10:37', ibis.time),
    ],
)
@pytest.mark.parametrize(
    ('left', 'right'), [(1, 'a'), ('a', 1), (1.0, 2.0), (['a'], [1])]
)
def test_between_time_failure_time(case, creator, left, right):
    value = creator(case)
    with pytest.raises(TypeError):
        value.between(left, right)


def test_custom_type_binary_operations():
    class Foo(ir.ValueExpr):
        def __add__(self, other):
            op = self.op()
            return type(op)(op.value + other).to_expr()

        __radd__ = __add__

    class FooNode(ops.ValueOp):
        value = Arg(rlz.integer)

        def output_type(self):
            return functools.partial(Foo, dtype=dt.int64)

    left = ibis.literal(2)
    right = FooNode(3).to_expr()
    result = left + right
    assert isinstance(result, Foo)
    assert isinstance(result.op(), FooNode)

    left = FooNode(3).to_expr()
    right = ibis.literal(2)
    result = left + right
    assert isinstance(result, Foo)
    assert isinstance(result.op(), FooNode)


def test_empty_array_as_argument():
    class Foo(ir.Expr):
        pass

    class FooNode(ops.ValueOp):
        value = Arg(rlz.value(dt.Array(dt.int64)))

        def output_type(self):
            return Foo

    node = FooNode([])
    value = node.value
    expected = literal([]).cast(dt.Array(dt.int64))

    assert value.type().equals(dt.Array(dt.null))
    assert value.cast(dt.Array(dt.int64)).equals(expected)


def test_struct_field_dir():
    t = ibis.table([('struct_col', 'struct<my_field: string>')])
    assert 'struct_col' in dir(t)
    assert 'my_field' in dir(t.struct_col)


def test_nullable_column_propagated():
    t = ibis.table(
        [
            ('a', dt.Int32(nullable=True)),
            ('b', dt.Int32(nullable=False)),
            ('c', dt.String(nullable=False)),
            ('d', dt.double),  # nullable by default
            ('f', dt.Double(nullable=False)),
        ]
    )

    assert t.a.type().nullable is True
    assert t.b.type().nullable is False
    assert t.c.type().nullable is False
    assert t.d.type().nullable is True
    assert t.f.type().nullable is False

    s = t.a + t.d
    assert s.type().nullable is True

    s = t.b + t.d
    assert s.type().nullable is True

    s = t.b + t.f
    assert s.type().nullable is False


@pytest.mark.parametrize(
    'base_expr',
    [
        ibis.table([('interval_col', dt.Interval(unit='D'))]).interval_col,
        ibis.interval(seconds=42),
    ],
)
def test_interval_negate(base_expr):
    expr = -base_expr
    expr2 = base_expr.negate()
    expr3 = ibis.negate(base_expr)
    assert isinstance(expr.op(), ops.Negate)
    assert expr.equals(expr2)
    assert expr.equals(expr3)


def test_large_timestamp():
    expr = ibis.timestamp('4567-02-03')
    expected = datetime(year=4567, month=2, day=3)
    result = expr.op().value
    assert result == expected


@pytest.mark.parametrize('tz', [None, 'UTC'])
def test_timestamp_with_timezone(tz):
    expr = ibis.timestamp('2017-01-01', timezone=tz)
    expected = pd.Timestamp('2017-01-01', tz=tz)
    result = expr.op().value
    assert expected == result


@pytest.mark.parametrize('tz', [None, 'UTC'])
def test_timestamp_timezone_type(tz):
    expr = ibis.timestamp('2017-01-01', timezone=tz)
    expected = dt.Timestamp(timezone=tz)
    assert expected == expr.op().dtype


def test_map_get_broadcast():
    t = ibis.table([('a', 'string')], name='t')
    lookup_table = ibis.literal({'a': 1, 'b': 2})
    expr = lookup_table.get(t.a)
    assert isinstance(expr, ir.IntegerColumn)


def test_map_getitem_broadcast():
    t = ibis.table([('a', 'string')], name='t')
    lookup_table = ibis.literal({'a': 1, 'b': 2})
    expr = lookup_table[t.a]
    assert isinstance(expr, ir.IntegerColumn)


def test_map_keys_output_type():
    mapping = ibis.literal({'a': 1, 'b': 2})
    assert mapping.keys().type() == dt.Array(dt.string)


def test_map_values_output_type():
    mapping = ibis.literal({'a': 1, 'b': 2})
    assert mapping.values().type() == dt.Array(dt.int8)


def test_scalar_isin_map_keys():
    mapping = ibis.literal({'a': 1, 'b': 2})
    key = ibis.literal('a')
    expr = key.isin(mapping.keys())
    assert isinstance(expr, ir.BooleanScalar)


def test_column_isin_map_keys():
    t = ibis.table([('a', 'string')], name='t')
    mapping = ibis.literal({'a': 1, 'b': 2})
    expr = t.a.isin(mapping.keys())
    assert isinstance(expr, ir.BooleanColumn)


def test_map_get_with_compatible_value_smaller():
    value = ibis.literal({'A': 1000, 'B': 2000})
    expr = value.get('C', 3)
    assert value.type() == dt.Map(dt.string, dt.int16)
    assert expr.type() == dt.int16


def test_map_get_with_compatible_value_bigger():
    value = ibis.literal({'A': 1, 'B': 2})
    expr = value.get('C', 3000)
    assert value.type() == dt.Map(dt.string, dt.int8)
    assert expr.type() == dt.int16


def test_map_get_with_incompatible_value_different_kind():
    value = ibis.literal({'A': 1000, 'B': 2000})
    with pytest.raises(IbisTypeError):
        value.get('C', 3.0)


@pytest.mark.parametrize('null_value', [None, ibis.NA])
def test_map_get_with_null_on_not_nullable(null_value):
    map_type = dt.Map(dt.string, dt.Int16(nullable=False))
    value = ibis.literal({'A': 1000, 'B': 2000}).cast(map_type)
    assert value.type() == map_type
    with pytest.raises(IbisTypeError):
        assert value.get('C', null_value)


@pytest.mark.parametrize('null_value', [None, ibis.NA])
def test_map_get_with_null_on_nullable(null_value):
    value = ibis.literal({'A': 1000, 'B': None})
    result = value.get('C', null_value)
    assert result.type().nullable


@pytest.mark.parametrize('null_value', [None, ibis.NA])
def test_map_get_with_null_on_null_type_with_null(null_value):
    value = ibis.literal({'A': None, 'B': None})
    result = value.get('C', null_value)
    assert result.type().nullable


def test_map_get_with_null_on_null_type_with_non_null():
    value = ibis.literal({'A': None, 'B': None})
    assert value.get('C', 1).type() == dt.int8


def test_map_get_with_incompatible_value():
    value = ibis.literal({'A': 1000, 'B': 2000})
    with pytest.raises(IbisTypeError):
        value.get('C', ['A'])


@pytest.mark.parametrize(
    ('value', 'expected_type'),
    [
        (datetime.now(), dt.timestamp),
        (datetime.now().date(), dt.date),
        (datetime.now().time(), dt.time),
    ],
)
def test_invalid_negate(value, expected_type):
    expr = ibis.literal(value)
    assert expr.type() == expected_type
    with pytest.raises(TypeError):
        -expr


@pytest.mark.parametrize(
    'type',
    [
        np.float16,
        np.float32,
        np.float64,
        np.int16,
        np.int32,
        np.int64,
        np.int64,
        np.int8,
        np.timedelta64,
        np.uint16,
        np.uint32,
        np.uint64,
        np.uint64,
        np.uint8,
        float,
        int,
    ],
)
def test_valid_negate(type):
    value = type(1)
    expr = ibis.literal(value)
    assert -expr is not None


@pytest.mark.xfail(
    reason='Type not supported in most backends', raises=TypeError
)
@pytest.mark.skipif(
    os.name == 'nt', reason='np.float128 not appear to exist on windows'
)
def test_valid_negate_float128():
    value = np.float128(1)
    expr = ibis.literal(value)
    assert -expr is not None


@pytest.mark.parametrize(
    ('kind', 'begin', 'end'),
    [
        ('preceding', None, None),
        ('preceding', 1, None),
        ('preceding', -1, 1),
        ('preceding', 1, -1),
        ('preceding', -1, -1),
        ('following', None, None),
        ('following', None, 1),
        ('following', -1, 1),
        ('following', 1, -1),
        ('following', -1, -1),
    ],
)
def test_window_unbounded_invalid(kind, begin, end):
    kwargs = {kind: (begin, end)}
    with pytest.raises(com.IbisInputError):
        ibis.window(**kwargs)


@pytest.mark.parametrize(
    ('left', 'right', 'expected'),
    [
        (ibis.literal(1), ibis.literal(1.0), dt.float64),
        (ibis.literal('a'), ibis.literal('b'), dt.string),
        (ibis.literal(1.0), ibis.literal(1), dt.float64),
        (ibis.literal(1), ibis.literal(1), dt.int8),
        (ibis.literal(1), ibis.literal(1000), dt.int16),
        (ibis.literal(2 ** 16), ibis.literal(2 ** 17), dt.int32),
        (ibis.literal(2 ** 50), ibis.literal(1000), dt.int64),
        (ibis.literal([1, 2]), ibis.literal([1, 2]), dt.Array(dt.int8)),
        (ibis.literal(['a']), ibis.literal([]), dt.Array(dt.string)),
        (ibis.literal([]), ibis.literal(['a']), dt.Array(dt.string)),
        (ibis.literal([]), ibis.literal([]), dt.Array(dt.null)),
    ],
)
def test_nullif_type(left, right, expected):
    assert left.nullif(right).type() == expected


@pytest.mark.parametrize(
    ('left', 'right'), [(ibis.literal(1), ibis.literal('a'))]
)
def test_nullif_fail(left, right):
    with pytest.raises(com.IbisTypeError):
        left.nullif(right)
    with pytest.raises(com.IbisTypeError):
        right.nullif(left)


@pytest.mark.parametrize(
    "join_method",
    [
        "left_join",
        pytest.param(
            "right_join",
            marks=pytest.mark.xfail(
                raises=AttributeError, reason="right_join is not an ibis API"
            ),
        ),
        "inner_join",
        "outer_join",
        "asof_join",
        pytest.param(
            "semi_join",
            marks=pytest.mark.xfail(
                raises=com.IbisTypeError,
                reason=(
                    "semi_join only gives access to the left table's "
                    "columns"
                ),
            ),
        ),
    ],
)
@pytest.mark.xfail(
    raises=(com.IbisError, AttributeError),
    reason="Select from unambiguous joins not implemented",
)
def test_select_on_unambiguous_join(join_method):
    t = ibis.table([("a0", dt.int64), ("b1", dt.string)], name="t")
    s = ibis.table([("a1", dt.int64), ("b2", dt.string)], name="s")
    method = getattr(t, join_method)
    join = method(s, t.b1 == s.b2)
    expr1 = join["a0", "a1"]
    expr2 = join[["a0", "a1"]]
    expr3 = join.select(["a0", "a1"])
    assert expr1.equals(expr2)
    assert expr1.equals(expr3)


def test_chained_select_on_join():
    t = ibis.table([("a", dt.int64)], name="t")
    s = ibis.table([("a", dt.int64), ("b", dt.string)], name="s")
    join = t.join(s)[t.a, s.b]
    expr1 = join["a", "b"]
    expr2 = join.select(["a", "b"])
    assert expr1.equals(expr2)


def test_repr_list_of_lists():
    lit = ibis.literal([[1]])
    result = repr(lit)
    expected = """\
Literal[array<array<int8>>]
  [[1]]"""
    assert result == expected


def test_repr_list_of_lists_in_table():
    t = ibis.table([('a', 'int64')], name='t')
    lit = ibis.literal([[1]])
    expr = t[t, lit.name('array_of_array')]
    result = repr(expr)
    expected = """\
ref_0
UnboundTable[table]
  name: t
  schema:
    a : int64

Selection[table]
  table:
    Table: ref_0
  selections:
    Table: ref_0
    array_of_array = Literal[array<array<int8>>]
      [[1]]"""
    assert result == expected

pandas.Timestamp