"""Unit tests for altair API"""

import io
import json
import operator
import os
import tempfile

import jsonschema
import pytest
import pandas as pd

import altair.vegalite.v4 as alt

try:
    import selenium
except ImportError:
    selenium = None


def getargs(*args, **kwargs):
    return args, kwargs


OP_DICT = {
    'layer': operator.add,
    'hconcat': operator.or_,
    'vconcat': operator.and_,
}


def _make_chart_type(chart_type):
    data = pd.DataFrame({
        'x': [28, 55, 43, 91, 81, 53, 19, 87],
        'y': [43, 91, 81, 53, 19, 87, 52, 28],
        'color': list('AAAABBBB'),
    })
    base = alt.Chart(data).mark_point().encode(
        x='x',
        y='y',
        color='color',
    )

    if chart_type in ['layer', 'hconcat', 'vconcat', 'concat']:
        func = getattr(alt, chart_type)
        return func(base.mark_square(), base.mark_circle())
    elif chart_type == 'facet':
        return base.facet('color')
    elif chart_type == 'facet_encoding':
        return base.encode(facet='color')
    elif chart_type == 'repeat':
        return base.encode(
            alt.X(alt.repeat(), type='quantitative')
        ).repeat(
            ['x', 'y']
        )
    elif chart_type == 'chart':
        return base
    else:
        raise ValueError("chart_type='{}' is not recognized".format(chart_type))


@pytest.fixture
def basic_chart():
    data = pd.DataFrame({
        'a': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
        'b': [28, 55, 43, 91, 81, 53, 19, 87, 52]
    })

    return alt.Chart(data).mark_bar().encode(
        x='a',
        y='b'
    )


def test_chart_data_types():
    Chart = lambda data: alt.Chart(data).mark_point().encode(x='x:Q', y='y:Q')

    # Url Data
    data = '/path/to/my/data.csv'
    dct = Chart(data).to_dict()
    assert dct['data'] == {'url': data}

    # Dict Data
    data = {"values": [{"x": 1, "y": 2}, {"x": 2, "y": 3}]}
    with alt.data_transformers.enable(consolidate_datasets=False):
        dct = Chart(data).to_dict()
    assert dct['data'] == data

    with alt.data_transformers.enable(consolidate_datasets=True):
        dct = Chart(data).to_dict()
    name = dct['data']['name']
    assert dct['datasets'][name] == data['values']

    # DataFrame data
    data = pd.DataFrame({"x": range(5), "y": range(5)})
    with alt.data_transformers.enable(consolidate_datasets=False):
        dct = Chart(data).to_dict()
    assert dct['data']['values'] == data.to_dict(orient='records')

    with alt.data_transformers.enable(consolidate_datasets=True):
        dct = Chart(data).to_dict()
    name = dct['data']['name']
    assert dct['datasets'][name] == data.to_dict(orient='records')

    # Named data object
    data = alt.NamedData(name='Foo')
    dct = Chart(data).to_dict()
    assert dct['data'] == {'name': 'Foo'}


def test_chart_infer_types():
    data = pd.DataFrame({'x': pd.date_range('2012', periods=10, freq='Y'),
                         'y': range(10),
                         'c': list('abcabcabca')})

    def _check_encodings(chart):
        dct = chart.to_dict()
        assert dct['encoding']['x']['type'] == 'temporal'
        assert dct['encoding']['x']['field'] == 'x'
        assert dct['encoding']['y']['type'] == 'quantitative'
        assert dct['encoding']['y']['field'] == 'y'
        assert dct['encoding']['color']['type'] == 'nominal'
        assert dct['encoding']['color']['field'] == 'c'

    # Pass field names by keyword
    chart = alt.Chart(data).mark_point().encode(x='x', y='y', color='c')
    _check_encodings(chart)

    # pass Channel objects by keyword
    chart = alt.Chart(data).mark_point().encode(x=alt.X('x'), y=alt.Y('y'),
                                                color=alt.Color('c'))
    _check_encodings(chart)

    # pass Channel objects by value
    chart = alt.Chart(data).mark_point().encode(alt.X('x'), alt.Y('y'),
                                                alt.Color('c'))
    _check_encodings(chart)

    # override default types
    chart = alt.Chart(data).mark_point().encode(alt.X('x', type='nominal'),
                                                alt.Y('y', type='ordinal'))
    dct = chart.to_dict()
    assert dct['encoding']['x']['type'] == 'nominal'
    assert dct['encoding']['y']['type'] == 'ordinal'


@pytest.mark.parametrize('args, kwargs', [
    getargs(detail=['value:Q', 'name:N'], tooltip=['value:Q', 'name:N']),
    getargs(detail=['value', 'name'], tooltip=['value', 'name']),
    getargs(alt.Detail(['value:Q', 'name:N']), alt.Tooltip(['value:Q', 'name:N'])),
    getargs(alt.Detail(['value', 'name']), alt.Tooltip(['value', 'name'])),
    getargs([alt.Detail('value:Q'), alt.Detail('name:N')],
            [alt.Tooltip('value:Q'), alt.Tooltip('name:N')]),
    getargs([alt.Detail('value'), alt.Detail('name')],
            [alt.Tooltip('value'), alt.Tooltip('name')]),
])
def test_multiple_encodings(args, kwargs):
    df = pd.DataFrame({
        'value': [1, 2, 3],
        'name': ['A', 'B', 'C'],
    })
    encoding_dct = [{'field': 'value', 'type': 'quantitative'},
                    {'field': 'name', 'type': 'nominal'}]
    chart = alt.Chart(df).mark_point().encode(*args, **kwargs)
    dct = chart.to_dict()
    assert dct['encoding']['detail'] == encoding_dct
    assert dct['encoding']['tooltip'] == encoding_dct


def test_chart_operations():
    data = pd.DataFrame({'x': pd.date_range('2012', periods=10, freq='Y'),
                         'y': range(10),
                         'c': list('abcabcabca')})
    chart1 = alt.Chart(data).mark_line().encode(x='x', y='y', color='c')
    chart2 = chart1.mark_point()
    chart3 = chart1.mark_circle()
    chart4 = chart1.mark_square()

    chart = chart1 + chart2 + chart3
    assert isinstance(chart, alt.LayerChart)
    assert len(chart.layer) == 3
    chart += chart4
    assert len(chart.layer) == 4

    chart = chart1 | chart2 | chart3
    assert isinstance(chart, alt.HConcatChart)
    assert len(chart.hconcat) == 3
    chart |= chart4
    assert len(chart.hconcat) == 4

    chart = chart1 & chart2 & chart3
    assert isinstance(chart, alt.VConcatChart)
    assert len(chart.vconcat) == 3
    chart &= chart4
    assert len(chart.vconcat) == 4


def test_selection_to_dict():
    brush = alt.selection(type='interval')

    # test some value selections
    # Note: X and Y cannot have conditions
    alt.Chart('path/to/data.json').mark_point().encode(
        color=alt.condition(brush, alt.ColorValue('red'), alt.ColorValue('blue')),
        opacity=alt.condition(brush, alt.value(0.5), alt.value(1.0)),
        text=alt.condition(brush, alt.TextValue('foo'), alt.value('bar'))
    ).to_dict()

    # test some field selections
    # Note: X and Y cannot have conditions
    # Conditions cannot both be fields
    alt.Chart('path/to/data.json').mark_point().encode(
        color=alt.condition(brush, alt.Color('col1:N'), alt.value('blue')),
        opacity=alt.condition(brush, 'col1:N', alt.value(0.5)),
        text=alt.condition(brush, alt.value('abc'), alt.Text('col2:N')),
        size=alt.condition(brush, alt.value(20), 'col2:N')
    ).to_dict()


def test_selection_expression():
    selection = alt.selection_single(fields=['value'])

    assert isinstance(selection.value, alt.expr.Expression)
    assert selection.value.to_dict() == '{0}.value'.format(selection.name)

    assert isinstance(selection['value'], alt.expr.Expression)
    assert selection['value'].to_dict() == "{0}['value']".format(selection.name)


@pytest.mark.parametrize('format', ['html', 'json', 'png', 'svg'])
@pytest.mark.skipif('not selenium')
def test_save(format, basic_chart):
    if format in ['html', 'json', 'svg']:
        out = io.StringIO()
        mode = 'r'
    else:
        out = io.BytesIO()
        mode = 'rb'
    fid, filename = tempfile.mkstemp(suffix='.' + format)

    try:
        try:
            basic_chart.save(out, format=format)
            basic_chart.save(filename)
        except ValueError as err:
            if str(err).startswith('Internet connection'):
                pytest.skip("web connection required for png/svg export")
            else:
                raise

        out.seek(0)
        with open(filename, mode) as f:
            assert f.read() == out.read()
    finally:
        os.remove(filename)

    out.seek(0)

    if format == 'json':
        spec = json.load(out)
        assert '$schema' in spec

    elif format == 'html':
        content = out.read()
        assert content.startswith('<!DOCTYPE html>')


def test_facet():
    # wrapped facet
    chart1 = alt.Chart('data.csv').mark_point().encode(
        x='x:Q',
        y='y:Q',
    ).facet(
        'category:N',
        columns=2
    )

    dct1 = chart1.to_dict()

    assert dct1['facet'] == alt.Facet('category:N').to_dict()
    assert dct1['columns'] == 2
    assert dct1['data'] == alt.UrlData('data.csv').to_dict()

    # explicit row/col facet
    chart2 = alt.Chart('data.csv').mark_point().encode(
        x='x:Q',
        y='y:Q',
    ).facet(
        row='category1:Q',
        column='category2:Q'
    )

    dct2 = chart2.to_dict()

    assert dct2['facet']['row'] == alt.Facet('category1:Q').to_dict()
    assert dct2['facet']['column'] == alt.Facet('category2:Q').to_dict()
    assert 'columns' not in dct2
    assert dct2['data'] == alt.UrlData('data.csv').to_dict()

def test_facet_parse():
    chart = alt.Chart('data.csv').mark_point().encode(
        x='x:Q',
        y='y:Q'
    ).facet(
        row='row:N',
        column='column:O'
    )
    dct = chart.to_dict()
    assert dct['data'] == {'url': 'data.csv'}
    assert 'data' not in dct['spec']
    assert dct['facet'] == {'column': {'field': 'column', 'type': 'ordinal'},
                            'row': {'field': 'row', 'type': 'nominal'}}


def test_facet_parse_data():
    data = pd.DataFrame({'x': range(5), 'y': range(5), 'row': list('abcab')})
    chart = alt.Chart(data).mark_point().encode(
        x='x',
        y='y:O'
    ).facet(
        row='row',
        column='column:O'
    )
    with alt.data_transformers.enable(consolidate_datasets=False):
        dct = chart.to_dict()
    assert 'values' in dct['data']
    assert 'data' not in dct['spec']
    assert dct['facet'] == {'column': {'field': 'column', 'type': 'ordinal'},
                            'row': {'field': 'row', 'type': 'nominal'}}

    with alt.data_transformers.enable(consolidate_datasets=True):
        dct = chart.to_dict()
    assert 'datasets' in dct
    assert 'name' in dct['data']
    assert 'data' not in dct['spec']
    assert dct['facet'] == {'column': {'field': 'column', 'type': 'ordinal'},
                            'row': {'field': 'row', 'type': 'nominal'}}


def test_selection():
    # test instantiation of selections
    interval = alt.selection_interval(name='selec_1')
    assert interval.selection.type == 'interval'
    assert interval.name == 'selec_1'

    single = alt.selection_single(name='selec_2')
    assert single.selection.type == 'single'
    assert single.name == 'selec_2'

    multi = alt.selection_multi(name='selec_3')
    assert multi.selection.type == 'multi'
    assert multi.name == 'selec_3'

    # test adding to chart
    chart = alt.Chart().add_selection(single)
    chart = chart.add_selection(multi, interval)
    assert set(chart.selection.keys()) == {'selec_1', 'selec_2', 'selec_3'}

    # test logical operations
    assert isinstance(single & multi, alt.Selection)
    assert isinstance(single | multi, alt.Selection)
    assert isinstance(~single, alt.Selection)
    assert isinstance((single & multi)[0].group, alt.SelectionAnd)
    assert isinstance((single | multi)[0].group, alt.SelectionOr)
    assert isinstance((~single)[0].group, alt.SelectionNot)

    # test that default names increment (regression for #1454)
    sel1 = alt.selection_single()
    sel2 = alt.selection_multi()
    sel3 = alt.selection_interval()
    names = {s.name for s in (sel1, sel2, sel3)}
    assert len(names) == 3


def test_transforms():
    # aggregate transform
    agg1 = alt.AggregatedFieldDef(**{'as': 'x1', 'op': 'mean', 'field': 'y'})
    agg2 = alt.AggregatedFieldDef(**{'as': 'x2', 'op': 'median', 'field': 'z'})
    chart = alt.Chart().transform_aggregate([agg1], ['foo'], x2='median(z)')
    kwds = dict(aggregate=[agg1, agg2], groupby=['foo'])
    assert chart.transform == [alt.AggregateTransform(**kwds)]

    # bin transform
    chart = alt.Chart().transform_bin("binned", field="field", bin=True)
    kwds = {'as': 'binned', 'field': 'field', 'bin': True}
    assert chart.transform == [alt.BinTransform(**kwds)]

    # calcualte transform
    chart = alt.Chart().transform_calculate("calc", "datum.a * 4")
    kwds = {'as': 'calc', 'calculate': 'datum.a * 4'}
    assert chart.transform == [alt.CalculateTransform(**kwds)]

    # density transform
    chart = alt.Chart().transform_density('x', as_=['value', 'density'])
    kwds = {'as': ['value', 'density'], 'density': 'x'}
    assert chart.transform == [alt.DensityTransform(**kwds)]

    # filter transform
    chart = alt.Chart().transform_filter("datum.a < 4")
    assert chart.transform == [alt.FilterTransform(filter="datum.a < 4")]

    # flatten transform
    chart = alt.Chart().transform_flatten(['A', 'B'], ['X', 'Y'])
    kwds = {'as': ['X', 'Y'], 'flatten': ['A', 'B']}
    assert chart.transform == [alt.FlattenTransform(**kwds)]

    # fold transform
    chart = alt.Chart().transform_fold(['A', 'B', 'C'], as_=['key', 'val'])
    kwds = {'as': ['key', 'val'], 'fold': ['A', 'B', 'C']}
    assert chart.transform == [alt.FoldTransform(**kwds)]

    # impute transform
    chart = alt.Chart().transform_impute("field", "key", groupby=["x"])
    kwds = {"impute": "field", "key": "key", "groupby": ["x"]}
    assert chart.transform == [alt.ImputeTransform(**kwds)]

    # joinaggregate transform
    chart = alt.Chart().transform_joinaggregate(min='min(x)', groupby=['key'])
    kwds = {
        'joinaggregate': [
            alt.JoinAggregateFieldDef(
                field='x',
                op='min',
                **{'as': 'min'})
        ],
        'groupby': ['key']
    }
    assert chart.transform == [alt.JoinAggregateTransform(**kwds)]

    # loess transform
    chart = alt.Chart().transform_loess('x', 'y', as_=['xx', 'yy'])
    kwds = {'on': 'x', 'loess': 'y', 'as': ['xx', 'yy']}
    assert chart.transform == [alt.LoessTransform(**kwds)]

    # lookup transform (data)
    lookup_data = alt.LookupData(alt.UrlData('foo.csv'), 'id', ['rate'])
    chart = alt.Chart().transform_lookup('a', from_=lookup_data,
                                         as_='a', default='b')
    kwds = {'from': lookup_data,
            'as': 'a',
            'lookup': 'a',
            'default': 'b'}
    assert chart.transform == [alt.LookupTransform(**kwds)]

    # lookup transform (selection)
    lookup_selection = alt.LookupSelection(key='key', selection='sel')
    chart = alt.Chart().transform_lookup('a', from_=lookup_selection,
                                         as_='a', default='b')
    kwds = {'from': lookup_selection,
            'as': 'a',
            'lookup': 'a',
            'default': 'b'}
    assert chart.transform == [alt.LookupTransform(**kwds)]

    # pivot transform
    chart = alt.Chart().transform_pivot('x', 'y')
    assert chart.transform == [alt.PivotTransform(pivot='x', value='y')]

    # quantile transform
    chart = alt.Chart().transform_quantile('x', as_=['prob', 'value'])
    kwds = {'quantile': 'x', 'as': ['prob', 'value']}
    assert chart.transform == [alt.QuantileTransform(**kwds)]

    # regression transform
    chart = alt.Chart().transform_regression('x', 'y', as_=['xx', 'yy'])
    kwds = {'on': 'x', 'regression': 'y', 'as': ['xx', 'yy']}
    assert chart.transform == [alt.RegressionTransform(**kwds)]

    # sample transform
    chart = alt.Chart().transform_sample()
    assert chart.transform == [alt.SampleTransform(1000)]

    # stack transform
    chart = alt.Chart().transform_stack('stacked', 'x', groupby=['y'])
    assert chart.transform == [alt.StackTransform(stack='x', groupby=['y'], **{'as': 'stacked'})]

    # timeUnit transform
    chart = alt.Chart().transform_timeunit("foo", field="x", timeUnit="date")
    kwds = {'as': 'foo', 'field': 'x', 'timeUnit': 'date'}
    assert chart.transform == [alt.TimeUnitTransform(**kwds)]

    # window transform
    chart = alt.Chart().transform_window(xsum='sum(x)', ymin='min(y)',
                                         frame=[None, 0])
    window = [alt.WindowFieldDef(**{'as': 'xsum', 'field': 'x', 'op': 'sum'}),
              alt.WindowFieldDef(**{'as': 'ymin', 'field': 'y', 'op': 'min'})]

    # kwargs don't maintain order in Python < 3.6, so window list can
    # be reversed
    assert (chart.transform == [alt.WindowTransform(frame=[None, 0],
                                                    window=window)]
            or chart.transform == [alt.WindowTransform(frame=[None, 0],
                                                       window=window[::-1])])


def test_filter_transform_selection_predicates():
    selector1 = alt.selection_interval(name='s1')
    selector2 = alt.selection_interval(name='s2')
    base = alt.Chart('data.txt').mark_point()

    chart = base.transform_filter(selector1)
    assert chart.to_dict()['transform'] == [{'filter': {'selection': 's1'}}]

    chart = base.transform_filter(~selector1)
    assert chart.to_dict()['transform'] == [{'filter': {'selection': {'not': 's1'}}}]

    chart = base.transform_filter(selector1 & selector2)
    assert chart.to_dict()['transform'] == [{'filter': {'selection': {'and': ['s1', 's2']}}}]

    chart = base.transform_filter(selector1 | selector2)
    assert chart.to_dict()['transform'] == [{'filter': {'selection': {'or': ['s1', 's2']}}}]

    chart = base.transform_filter(selector1 | ~selector2)
    assert chart.to_dict()['transform'] == [{'filter': {'selection': {'or': ['s1', {'not': 's2'}]}}}]

    chart = base.transform_filter(~selector1 | ~selector2)
    assert chart.to_dict()['transform'] == [{'filter': {'selection': {'or': [{'not': 's1'}, {'not': 's2'}]}}}]

    chart = base.transform_filter(~(selector1 & selector2))
    assert chart.to_dict()['transform'] == [{'filter': {'selection': {'not': {'and': ['s1', 's2']}}}}]        



def test_resolve_methods():
    chart = alt.LayerChart().resolve_axis(x='shared', y='independent')
    assert chart.resolve == alt.Resolve(axis=alt.AxisResolveMap(x='shared', y='independent'))

    chart = alt.LayerChart().resolve_legend(color='shared', fill='independent')
    assert chart.resolve == alt.Resolve(legend=alt.LegendResolveMap(color='shared', fill='independent'))

    chart = alt.LayerChart().resolve_scale(x='shared', y='independent')
    assert chart.resolve == alt.Resolve(scale=alt.ScaleResolveMap(x='shared', y='independent'))


def test_layer_encodings():
    chart = alt.LayerChart().encode(x='column:Q')
    assert chart.encoding.x == alt.X(shorthand='column:Q')


def test_add_selection():
    selections = [alt.selection_interval(),
                  alt.selection_single(),
                  alt.selection_multi()]
    chart = alt.Chart().mark_point().add_selection(
        selections[0]
    ).add_selection(
        selections[1],
        selections[2]
    )
    expected = {s.name: s.selection for s in selections}
    assert chart.selection == expected


def test_repeat_add_selections():
    base = alt.Chart('data.csv').mark_point()
    selection = alt.selection_single()
    chart1 = base.add_selection(selection).repeat(list('ABC'))
    chart2 = base.repeat(list('ABC')).add_selection(selection)
    assert chart1.to_dict() == chart2.to_dict()


def test_facet_add_selections():
    base = alt.Chart('data.csv').mark_point()
    selection = alt.selection_single()
    chart1 = base.add_selection(selection).facet('val:Q')
    chart2 = base.facet('val:Q').add_selection(selection)
    assert chart1.to_dict() == chart2.to_dict()


def test_layer_add_selection():
    base = alt.Chart('data.csv').mark_point()
    selection = alt.selection_single()
    chart1 = alt.layer(base.add_selection(selection), base)
    chart2 = alt.layer(base, base).add_selection(selection)
    assert chart1.to_dict() == chart2.to_dict()


@pytest.mark.parametrize('charttype', [alt.concat, alt.hconcat, alt.vconcat])
def test_compound_add_selections(charttype):
    base = alt.Chart('data.csv').mark_point()
    selection = alt.selection_single()
    chart1 = charttype(
        base.add_selection(selection),
        base.add_selection(selection)
    )
    chart2 = charttype(base, base).add_selection(selection)
    assert chart1.to_dict() == chart2.to_dict()


def test_selection_property():
    sel = alt.selection_interval()
    chart = alt.Chart('data.csv').mark_point().properties(
        selection=sel
    )

    assert list(chart['selection'].keys()) == [sel.name]


def test_LookupData():
    df = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]})
    lookup = alt.LookupData(data=df, key='x')

    dct = lookup.to_dict()
    assert dct['key'] == 'x'
    assert dct['data'] == {'values': [{'x': 1, 'y': 4},
                                      {'x': 2, 'y': 5},
                                      {'x': 3, 'y': 6}]}


def test_themes():
    chart = alt.Chart('foo.txt').mark_point()

    with alt.themes.enable('default'):
        assert chart.to_dict()['config'] == {"view": {"continuousWidth": 400, "continuousHeight": 300}}

    with alt.themes.enable('opaque'):
        assert chart.to_dict()['config'] == {"background": "white",
                                             "view": {"continuousWidth": 400, "continuousHeight": 300}}

    with alt.themes.enable('none'):
        assert 'config' not in chart.to_dict()


def test_chart_from_dict():
    base = alt.Chart('data.csv').mark_point().encode(x='x:Q', y='y:Q')

    charts = [base,
              base + base,
              base | base,
              base & base,
              base.facet('c:N'),
              (base + base).facet(row='c:N', data='data.csv'),
              base.repeat(['c', 'd']),
              (base + base).repeat(row=['c', 'd'])
    ]

    for chart in charts:
        chart_out = alt.Chart.from_dict(chart.to_dict())
        assert type(chart_out) is type(chart)

    # test that an invalid spec leads to a schema validation error
    with pytest.raises(jsonschema.ValidationError):
        alt.Chart.from_dict({'invalid': 'spec'})


def test_consolidate_datasets(basic_chart):
    subchart1 = basic_chart
    subchart2 = basic_chart.copy()
    subchart2.data = basic_chart.data.copy()
    chart = subchart1 | subchart2

    with alt.data_transformers.enable(consolidate_datasets=True):
        dct_consolidated = chart.to_dict()

    with alt.data_transformers.enable(consolidate_datasets=False):
        dct_standard = chart.to_dict()

    assert 'datasets' in dct_consolidated
    assert 'datasets' not in dct_standard

    datasets = dct_consolidated['datasets']

    # two dataset copies should be recognized as duplicates
    assert len(datasets) == 1

    # make sure data matches original & names are correct
    name, data = datasets.popitem()

    for spec in dct_standard['hconcat']:
        assert spec['data']['values'] == data

    for spec in dct_consolidated['hconcat']:
        assert spec['data'] == {'name': name}


def test_consolidate_InlineData():
    data = alt.InlineData(
        values=[{'a': 1, 'b': 1}, {'a': 2, 'b': 2}],
        format={'type': 'csv'}
    )
    chart = alt.Chart(data).mark_point()

    with alt.data_transformers.enable(consolidate_datasets=False):
        dct = chart.to_dict()
    assert dct['data']['format'] == data.format
    assert dct['data']['values'] == data.values

    with alt.data_transformers.enable(consolidate_datasets=True):
        dct = chart.to_dict()
    assert dct['data']['format'] == data.format
    assert list(dct['datasets'].values())[0] == data.values

    data = alt.InlineData(
        values=[],
        name='runtime_data'
    )
    chart = alt.Chart(data).mark_point()

    with alt.data_transformers.enable(consolidate_datasets=False):
        dct = chart.to_dict()
    assert dct['data'] == data.to_dict()

    with alt.data_transformers.enable(consolidate_datasets=True):
        dct = chart.to_dict()
    assert dct['data'] == data.to_dict()


def test_repeat():
    # wrapped repeat
    chart1 = alt.Chart('data.csv').mark_point().encode(
        x=alt.X(alt.repeat(), type='quantitative'),
        y='y:Q',
    ).repeat(
        ['A', 'B', 'C', 'D'],
        columns=2
    )

    dct1 = chart1.to_dict()

    assert dct1['repeat'] == ['A', 'B', 'C', 'D']
    assert dct1['columns'] == 2
    assert dct1['spec']['encoding']['x']['field'] == {'repeat': 'repeat'}

    # explicit row/col repeat
    chart2 = alt.Chart('data.csv').mark_point().encode(
        x=alt.X(alt.repeat('row'), type='quantitative'),
        y=alt.Y(alt.repeat('column'), type='quantitative')
    ).repeat(
        row=['A', 'B', 'C'],
        column=['C', 'B', 'A']
    )

    dct2 = chart2.to_dict()

    assert dct2['repeat'] == {'row': ['A', 'B', 'C'], 'column': ['C', 'B', 'A']}
    assert 'columns' not in dct2
    assert dct2['spec']['encoding']['x']['field'] == {'repeat': 'row'}
    assert dct2['spec']['encoding']['y']['field'] == {'repeat': 'column'}


def test_data_property():
    data = pd.DataFrame({'x': [1, 2, 3], 'y': list('ABC')})
    chart1 = alt.Chart(data).mark_point()
    chart2 = alt.Chart().mark_point().properties(data=data)

    assert chart1.to_dict() == chart2.to_dict()

    
@pytest.mark.parametrize('method', ['layer', 'hconcat', 'vconcat', 'concat'])
@pytest.mark.parametrize('data', ['data.json', pd.DataFrame({'x': range(3), 'y': list('abc')})])
def test_subcharts_with_same_data(method, data):
    func = getattr(alt, method)

    point = alt.Chart(data).mark_point().encode(x='x:Q', y='y:Q')
    line = point.mark_line()
    text = point.mark_text()

    chart1 = func(point, line, text)
    assert chart1.data is not alt.Undefined
    assert all(c.data is alt.Undefined for c in getattr(chart1, method))

    if method != 'concat':
        op = OP_DICT[method]
        chart2 = op(op(point, line), text)
        assert chart2.data is not alt.Undefined
        assert all(c.data is alt.Undefined for c in getattr(chart2, method))


@pytest.mark.parametrize('method', ['layer', 'hconcat', 'vconcat', 'concat'])
@pytest.mark.parametrize('data', ['data.json', pd.DataFrame({'x': range(3), 'y': list('abc')})])
def test_subcharts_different_data(method, data):
    func = getattr(alt, method)

    point = alt.Chart(data).mark_point().encode(x='x:Q', y='y:Q')
    otherdata = alt.Chart('data.csv').mark_point().encode(x='x:Q', y='y:Q')
    nodata = alt.Chart().mark_point().encode(x='x:Q', y='y:Q')

    chart1 = func(point, otherdata)
    assert chart1.data is alt.Undefined
    assert getattr(chart1, method)[0].data is data

    chart2 = func(point, nodata)
    assert chart2.data is alt.Undefined
    assert getattr(chart2, method)[0].data is data


def test_layer_facet(basic_chart):
    chart = (basic_chart + basic_chart).facet(row='row:Q')
    assert chart.data is not alt.Undefined
    assert chart.spec.data is alt.Undefined
    for layer in chart.spec.layer:
        assert layer.data is alt.Undefined

    dct = chart.to_dict()
    assert 'data' in dct


def test_layer_errors():
    toplevel_chart = alt.Chart(
        'data.txt'
    ).mark_point().configure_legend(
        columns=2
    )

    facet_chart1 = alt.Chart('data.txt').mark_point().encode(
        facet='row:Q'
    )

    facet_chart2 = alt.Chart('data.txt').mark_point().facet('row:Q')

    repeat_chart = alt.Chart('data.txt').mark_point().repeat(['A', 'B', 'C'])

    simple_chart = alt.Chart('data.txt').mark_point()

    with pytest.raises(ValueError) as err:
        toplevel_chart + simple_chart
    assert str(err.value).startswith(
        'Objects with "config" attribute cannot be used within LayerChart.'
    )

    with pytest.raises(ValueError) as err:
        repeat_chart + simple_chart
    assert str(err.value) == "Repeat charts cannot be layered."

    with pytest.raises(ValueError) as err:
        facet_chart1 + simple_chart
    assert str(err.value) == "Faceted charts cannot be layered."

    with pytest.raises(ValueError) as err:
        alt.layer(simple_chart) + facet_chart2
    assert str(err.value) == "Faceted charts cannot be layered."


@pytest.mark.parametrize(
    'chart_type',
    ['layer', 'hconcat', 'vconcat', 'concat', 'facet', 'facet_encoding', 'repeat']
)
def test_resolve(chart_type):
    chart = _make_chart_type(chart_type)
    chart = chart.resolve_scale(
        x='independent',
    ).resolve_legend(
        color='independent'
    ).resolve_axis(
        y='independent'
    )
    dct = chart.to_dict()
    assert dct['resolve'] == {
        'scale': {'x': 'independent'},
        'legend': {'color': 'independent'},
        'axis': {'y': 'independent'},
    }


# TODO: test vconcat, hconcat, concat when schema allows them.
# This is blocked by https://github.com/vega/vega-lite/issues/5261
@pytest.mark.parametrize('chart_type', ['chart', 'layer', 'facet_encoding'])
@pytest.mark.parametrize('facet_arg', [None, 'facet', 'row', 'column'])
def test_facet(chart_type, facet_arg):
    chart = _make_chart_type(chart_type)
    if facet_arg is None:
        chart = chart.facet('color:N', columns=2)
    else:
        chart = chart.facet(**{facet_arg: 'color:N', 'columns': 2})
    dct = chart.to_dict()

    assert 'spec' in dct
    assert dct['columns'] == 2
    expected = {'field': 'color', 'type': 'nominal'}
    if facet_arg is None or facet_arg == 'facet':
        assert dct['facet'] == expected
    else:
        assert dct['facet'][facet_arg] == expected


def test_sequence():
    data = alt.sequence(100)
    assert data.to_dict() == {'sequence': {'start': 0, 'stop': 100}}

    data = alt.sequence(5, 10)
    assert data.to_dict() == {'sequence': {'start': 5, 'stop': 10}}

    data = alt.sequence(0, 1, 0.1, as_='x')
    assert data.to_dict() == {'sequence': {'start': 0, 'stop': 1, 'step': 0.1, 'as': 'x'}}


def test_graticule():
    data = alt.graticule()
    assert data.to_dict() == {'graticule': True}

    data = alt.graticule(step=[15, 15])
    assert data.to_dict() == {'graticule': {'step': [15, 15]}}


def test_sphere():
    data = alt.sphere()
    assert data.to_dict() == {'sphere': True}
