#  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
#  ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
#  ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
#  ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
#  ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
#  ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
#  ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
#  ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
#  ┃ This file is part of the Perspective library, distributed under the terms ┃
#  ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
#  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import pyarrow as pa
from datetime import date, datetime
import perspective as psp

client = psp.Server().new_local_client()
Table = client.table


class TestToArrow(object):
    def test_to_arrow_nones_symmetric(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow()
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_big_numbers_symmetric(self):
        data = {
            "a": [1, 2, 3, 4],
            "b": [
                1.7976931348623157e308,
                1.7976931348623157e308,
                1.7976931348623157e308,
                1.7976931348623157e308,
            ],
        }
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow()
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_boolean_symmetric(self):
        data = {"a": [True, False, None, False, True, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "boolean"}
        arr = tbl.view().to_arrow()
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_str_symmetric(self):
        data = {"a": ["a", "b", "c", "d", "e", None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "string"}
        arr = tbl.view().to_arrow()
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_str_dict(self):
        data = {
            "a": ["abcdefg", "abcdefg", "h"],
            "b": ["aaa", "bbb", "bbb"],
            "c": ["hello", "world", "world"],
        }
        tbl = Table(data)
        assert tbl.schema() == {"a": "string", "b": "string", "c": "string"}
        arr = tbl.view().to_arrow()

        # assert that we are actually generating dict arrays
        buf = pa.BufferReader(arr)
        reader = pa.ipc.open_stream(buf)
        arrow_table = reader.read_all()
        arrow_schema = arrow_table.schema

        for name in ("a", "b", "c"):
            arrow_type = arrow_schema.field(name).type
            assert pa.types.is_dictionary(arrow_type)

        # assert that data is symmetrical
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_date_symmetric(self):
        # data = {"a": [date(2019, 7, 11), date(2016, 2, 29), date(2019, 12, 10)]}
        # tbl = Table(data)
        tbl = Table({"a": "date"})
        # data = {"a": map(lambda x: x.getTime(), [date(2019, 7, 11), date(2016, 2, 29), date(2019, 12, 10)])}
        data = {"a": [date(2019, 7, 11), date(2016, 2, 29), date(2019, 12, 10)]}
        tbl.update(data)
        assert tbl.schema() == {"a": "date"}
        arr = tbl.view().to_arrow()
        tbl2 = Table(arr)

        def ts(x):
            return int(datetime.timestamp(x) * 1000)

        assert tbl2.schema() == tbl.schema()
        assert tbl2.view().to_columns() == {
            "a": [
                ts(datetime(2019, 7, 11)),
                ts(datetime(2016, 2, 29)),
                ts(datetime(2019, 12, 10)),
            ]
        }

    def test_to_arrow_date_symmetric_january(self, util):
        data = {"a": [date(2019, 1, 1), date(2016, 1, 1), date(2019, 1, 1)]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "date"}
        arr = tbl.view().to_arrow()
        tbl2 = Table(arr)
        assert tbl2.schema() == tbl.schema()
        assert tbl2.view().to_columns() == {
            "a": [
                util.to_timestamp(x)
                for x in [
                    datetime(2019, 1, 1),
                    datetime(2016, 1, 1),
                    datetime(2019, 1, 1),
                ]
            ]
        }

    def test_to_arrow_datetime_symmetric(self, util):
        data = {
            "a": [
                datetime(2019, 7, 11, 12, 30),
                datetime(2016, 2, 29, 11, 0),
                datetime(2019, 12, 10, 12, 0),
            ]
        }
        tbl = Table(data)
        assert tbl.schema() == {"a": "datetime"}
        arr = tbl.view().to_arrow()
        tbl2 = Table(arr)
        assert tbl2.schema() == tbl.schema()
        assert tbl2.view().to_columns() == {
            "a": [
                util.to_timestamp(x)
                for x in [
                    datetime(2019, 7, 11, 12, 30),
                    datetime(2016, 2, 29, 11, 0),
                    datetime(2019, 12, 10, 12, 0),
                ]
            ]
        }

    def test_to_arrow_one_symmetric(self):
        data = {
            "a": [1, 2, 3, 4],
            "b": ["a", "b", "c", "d"],
            "c": [
                datetime(2019, 7, 11, 12, 0),
                datetime(2019, 7, 11, 12, 10),
                datetime(2019, 7, 11, 12, 20),
                datetime(2019, 7, 11, 12, 30),
            ],
        }
        tbl = Table(data)
        view = tbl.view(group_by=["a"])
        arrow = view.to_arrow()
        tbl2 = Table(arrow)
        assert tbl2.schema() == {
            "a (Group by 1)": "integer",
            "a": "integer",
            "b": "integer",
            "c": "integer",
        }
        d = view.to_columns()
        d["a (Group by 1)"] = [
            x[0] if len(x) > 0 else None for x in d.pop("__ROW_PATH__")
        ]
        assert tbl2.view().to_columns() == d

    def test_to_arrow_two_symmetric(self):
        data = {
            "a": [1, 2, 3, 4],
            "b": ["hello", "world", "hello2", "world2"],
            "c": [datetime(2019, 7, 11, 12, i) for i in range(0, 40, 10)],
        }
        tbl = Table(data)
        view = tbl.view(group_by=["a"], split_by=["b"])
        arrow = view.to_arrow()
        tbl2 = Table(arrow)
        assert tbl2.schema() == {
            "a (Group by 1)": "integer",
            "hello|a": "integer",
            "hello|b": "integer",
            "hello|c": "integer",
            "world|a": "integer",
            "world|b": "integer",
            "world|c": "integer",
            "hello2|a": "integer",
            "hello2|b": "integer",
            "hello2|c": "integer",
            "world2|a": "integer",
            "world2|b": "integer",
            "world2|c": "integer",
        }
        d = view.to_columns()
        d["a (Group by 1)"] = [
            x[0] if len(x) > 0 else None for x in d.pop("__ROW_PATH__")
        ]
        assert tbl2.view().to_columns() == d

    def test_to_arrow_column_only_symmetric(self):
        data = {
            "a": [1, 2, 3, 4],
            "b": ["a", "b", "c", "d"],
            "c": [datetime(2019, 7, 11, 12, i) for i in range(0, 40, 10)],
        }
        tbl = Table(data)
        view = tbl.view(split_by=["a"])
        arrow = view.to_arrow()
        tbl2 = Table(arrow)
        assert tbl2.schema() == {
            "1|a": "integer",
            "1|b": "string",
            "1|c": "datetime",
            "2|a": "integer",
            "2|b": "string",
            "2|c": "datetime",
            "3|a": "integer",
            "3|b": "string",
            "3|c": "datetime",
            "4|a": "integer",
            "4|b": "string",
            "4|c": "datetime",
        }
        d = view.to_columns()
        assert tbl2.view().to_columns() == d

    # start and end row
    def test_to_arrow_start_row(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_row=3)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == {"a": data["a"][3:], "b": data["b"][3:]}

    def test_to_arrow_end_row(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(end_row=2)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == {"a": data["a"][:2], "b": data["b"][:2]}

    def test_to_arrow_start_end_row(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_row=2, end_row=3)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == {"a": data["a"][2:3], "b": data["b"][2:3]}

    def test_to_arrow_start_end_row_equiv(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_row=2, end_row=2)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == {"a": [], "b": []}

    def test_to_arrow_start_row_invalid(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_row=-1)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_end_row_invalid(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(end_row=6)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_start_end_row_invalid(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_row=-1, end_row=6)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_start_col(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_col=1)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == {"b": data["b"]}

    def test_to_arrow_end_col(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(end_col=1)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == {"a": data["a"]}

    def test_to_arrow_start_end_col(self):
        data = {
            "a": [None, 1, None, 2, 3],
            "b": [1.5, 2.5, None, 3.5, None],
            "c": [None, 1, None, 2, 3],
            "d": [1.5, 2.5, None, 3.5, None],
        }
        tbl = Table(data)
        assert tbl.schema() == {
            "a": "integer",
            "b": "float",
            "c": "integer",
            "d": "float",
        }
        arr = tbl.view().to_arrow(start_col=1, end_col=3)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == {"b": data["b"], "c": data["c"]}

    def test_to_arrow_start_col_invalid(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_col=-1)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_end_col_invalid(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(end_col=6)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_start_end_col_invalid(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_col=-1, end_col=6)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == data

    def test_to_arrow_start_end_col_equiv_row(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_col=1, end_col=1, start_row=2, end_row=3)
        tbl2 = Table(arr)
        # start/end col is a range - thus start=end provides no columns
        assert tbl2.view().to_columns() == {}

    def test_to_arrow_start_end_col_equiv(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(start_col=1, end_col=1)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == {}

    def test_to_arrow_start_end_row_end_col(self):
        data = {"a": [None, 1, None, 2, 3], "b": [1.5, 2.5, None, 3.5, None]}
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float"}
        arr = tbl.view().to_arrow(end_col=1, start_row=2, end_row=3)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == tbl.view().to_columns(
            end_col=1, start_row=2, end_row=3
        )

    def test_to_arrow_start_end_col_start_row(self):
        data = {
            "a": [None, 1, None, 2, 3],
            "b": [1.5, 2.5, None, 3.5, None],
            "c": [1.5, 2.5, None, 4.5, None],
        }
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float", "c": "float"}
        arr = tbl.view().to_arrow(start_col=1, end_col=2, start_row=2)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == tbl.view().to_columns(
            start_col=1, end_col=2, start_row=2
        )

    def test_to_arrow_start_end_col_end_row(self):
        data = {
            "a": [None, 1, None, 2, 3],
            "b": [1.5, 2.5, None, 3.5, None],
            "c": [1.5, 2.5, None, 4.5, None],
        }
        tbl = Table(data)
        assert tbl.schema() == {"a": "integer", "b": "float", "c": "float"}
        arr = tbl.view().to_arrow(start_col=1, end_col=2, end_row=2)
        tbl2 = Table(arr)
        assert tbl2.view().to_columns() == tbl.view().to_columns(
            start_col=1, end_col=2, end_row=2
        )

    def test_to_arrow_one_mean(self):
        data = {"a": [1, 2, 3, 4], "b": ["a", "a", "b", "b"]}

        table = Table(data)
        view = table.view(group_by=["b"], columns=["a"], aggregates={"a": "mean"})
        arrow = view.to_arrow()

        table2 = Table(arrow)
        view2 = table2.view()
        result = view2.to_columns()

        assert result == {"b (Group by 1)": [None, "a", "b"], "a": [2.5, 1.5, 3.5]}
