from datetime import date, datetime, time as dt_time
from pathlib import Path

import numpy as np
import param
import pytest

from bokeh.models.widgets import FileInput as BkFileInput

from panel import config
from panel.widgets import (
    ArrayInput, Checkbox, DatePicker, DateRangePicker, DatetimeInput,
    DatetimePicker, DatetimeRangeInput, DatetimeRangePicker, FileInput,
    FloatInput, IntInput, LiteralInput, StaticText, TextInput, TimePicker,
)


def test_checkbox(document, comm):

    checkbox = Checkbox(value=True, name='Checkbox')

    widget = checkbox.get_root(document, comm=comm)

    assert isinstance(widget, checkbox._widget_type)
    assert widget.label == 'Checkbox'
    assert widget.active == True

    widget.active = False
    checkbox._process_events({'active': False})
    assert checkbox.value == False

    checkbox.value = True
    assert widget.active == True


def test_date_picker(document, comm):
    date_picker = DatePicker(name='DatePicker', value=date(2018, 9, 2),
                             start=date(2018, 9, 1), end=date(2018, 9, 10))

    widget = date_picker.get_root(document, comm=comm)

    assert isinstance(widget, date_picker._widget_type)
    assert widget.title == 'DatePicker'
    assert widget.value == '2018-09-02'
    assert widget.min_date == '2018-09-01'
    assert widget.max_date == '2018-09-10'

    widget.value = '2018-09-03'
    date_picker._process_events({'value': '2018-09-03'})
    assert date_picker.value == date(2018, 9, 3)

    date_picker._process_events({'value': date(2018, 9, 5)})
    assert date_picker.value == date(2018, 9, 5)

    date_picker._process_events({'value': date(2018, 9, 6)})
    assert date_picker.value == date(2018, 9, 6)

    date_picker.value = date(2018, 9, 4)
    assert widget.value == '2018-09-04'


def test_date_picker_options(document, comm):
    options = [date(2018, 9, 1), date(2018, 9, 2), date(2018, 9, 3)]
    datetime_picker = DatePicker(
        name='DatetimePicker', value=date(2018, 9, 2),
        options=options
    )
    assert datetime_picker.value == date(2018, 9, 2)
    assert datetime_picker.enabled_dates == options


def test_daterange_picker(document, comm):
    date_range_picker = DateRangePicker(name='DateRangePicker',
                                        value=(date(2018, 9, 2), date(2018, 9, 3)),
                                        start=date(2018, 9, 1),
                                        end=date(2018, 9, 10))

    widget = date_range_picker.get_root(document, comm=comm)

    assert isinstance(widget, date_range_picker._widget_type)
    assert widget.title == 'DateRangePicker'
    assert widget.value == ('2018-09-02', '2018-09-03')
    assert widget.min_date == '2018-09-01'
    assert widget.max_date == '2018-09-10'

    date_range_picker._process_events({'value': ('2018-09-03', '2018-09-04')})
    assert date_range_picker.value == (date(2018, 9, 3), date(2018, 9, 4))

    date_range_picker._process_events({'value': ('2018-09-05', '2018-09-08')})
    assert date_range_picker.value == (date(2018, 9, 5), date(2018, 9, 8))

    value = date_range_picker._process_param_change({'value': (date(2018, 9, 4), date(2018, 9, 5))})
    assert value['value'] == ('2018-09-04', '2018-09-05')

    value = date(2018, 9, 4)
    assert date_range_picker._convert_date_to_string(value) == '2018-09-04'
    assert date_range_picker._convert_string_to_date(date_range_picker._convert_date_to_string(value)) == value

    # Check start value
    with pytest.raises(ValueError):
        date_range_picker._process_events({'value': ('2018-08-31', '2018-09-01')})

    # Check end value
    with pytest.raises(ValueError):
        date_range_picker._process_events({'value': ('2018-09-10', '2018-09-11')})


def test_datetime_picker(document, comm):
    datetime_picker = DatetimePicker(
        name='DatetimePicker', value=datetime(2018, 9, 2, 1, 5),
        start=date(2018, 9, 1), end=datetime(2018, 9, 10)
    )

    widget = datetime_picker.get_root(document, comm=comm)

    assert isinstance(widget, datetime_picker._widget_type)
    assert widget.title == 'DatetimePicker'
    assert widget.value == '2018-09-02 01:05:00'
    assert widget.min_date == '2018-09-01T00:00:00'
    assert widget.max_date == '2018-09-10T00:00:00'

    datetime_picker._process_events({'value': '2018-09-03 03:00:01'})
    assert datetime_picker.value == datetime(2018, 9, 3, 3, 0, 1)

    datetime_picker._process_events({'value': datetime(2018, 9, 5)})
    assert datetime_picker.value == datetime(2018, 9, 5)

    value = datetime_picker._process_param_change({'value': datetime(2018, 9, 4, 1, 0, 1)})
    assert value['value'] == '2018-09-04 01:00:01'

    value = datetime(2018, 9, 4, 12, 1)
    assert datetime_picker._deserialize_value(value) == '2018-09-04 12:01:00'
    assert datetime_picker._serialize_value(datetime_picker._deserialize_value(value)) == value

    # Check start value
    with pytest.raises(ValueError):
        datetime_picker._process_events({'value': '2018-08-31 23:59:59'})

    # Check end value
    with pytest.raises(ValueError):
        datetime_picker._process_events({'value': '2018-09-10 00:00:01'})


def test_datetime_picker_options(document, comm):
    options = [datetime(2018, 9, 1), datetime(2018, 9, 2), datetime(2018, 9, 3)]
    datetime_picker = DatetimePicker(
        name='DatetimePicker', value=datetime(2018, 9, 2, 1, 5),
        options=options
    )
    assert datetime_picker.value == datetime(2018, 9, 2, 1, 5)
    assert datetime_picker.enabled_dates == options


def test_datetime_range_picker(document, comm):
    datetime_range_picker = DatetimeRangePicker(
        name='DatetimeRangePicker', value=(datetime(2018, 9, 2, 1, 5), datetime(2018, 9, 2, 1, 6)),
        start=date(2018, 9, 1), end=datetime(2018, 9, 10)
    )

    widget = datetime_range_picker.get_root(document, comm=comm)

    assert isinstance(widget, datetime_range_picker._widget_type)
    assert widget.title == 'DatetimeRangePicker'
    assert widget.value == '2018-09-02 01:05:00 to 2018-09-02 01:06:00'
    assert widget.min_date == '2018-09-01T00:00:00'
    assert widget.max_date == '2018-09-10T00:00:00'

    datetime_range_picker._process_events({'value': '2018-09-03 03:00:01 to 2018-09-04 03:00:01'})
    assert datetime_range_picker.value == (datetime(2018, 9, 3, 3, 0, 1), datetime(2018, 9, 4, 3, 0, 1))

    value = datetime_range_picker._process_param_change(
        {'value': (datetime(2018, 9, 4, 1, 0, 1), datetime(2018, 9, 4, 4, 0, 1))}
    )
    assert value['value'] == '2018-09-04 01:00:01 to 2018-09-04 04:00:01'

    value = (datetime(2018, 9, 4, 12, 1), datetime(2018, 9, 4, 12, 1, 10))
    assert datetime_range_picker._deserialize_value(value) == '2018-09-04 12:01:00 to 2018-09-04 12:01:10'
    assert datetime_range_picker._serialize_value(datetime_range_picker._deserialize_value(value)) == value

    # Check start value
    with pytest.raises(ValueError):
        datetime_range_picker._process_events({'value': '2018-08-31 23:59:59'})

    # Check end value
    with pytest.raises(ValueError):
        datetime_range_picker._process_events({'value': '2018-09-10 00:00:01'})


def test_time_picker(document, comm):
    time_picker = TimePicker(name='Time Picker', value=dt_time(hour=18), format='H:i K')
    assert time_picker.value == dt_time(hour=18)
    assert time_picker.format == 'H:i K'
    assert time_picker.start is None
    assert time_picker.end is None


def test_time_picker_str(document, comm):
    time_picker = TimePicker(name='Time Picker', value="08:28", start='00:00', end='12:00')
    assert time_picker.value == "08:28"
    assert time_picker.format == 'H:i'
    assert time_picker.start == "00:00"
    assert time_picker.end == "12:00"


def test_file_input(document, comm):
    file_input = FileInput(accept='.txt')

    widget = file_input.get_root(document, comm=comm)

    assert isinstance(widget, BkFileInput)

    file_input._process_events({'mime_type': 'text/plain', 'value': 'U29tZSB0ZXh0Cg==', 'filename': 'testfile'})
    assert file_input.value == b'Some text\n'
    assert file_input.mime_type == 'text/plain'
    assert file_input.accept == '.txt'
    assert file_input.filename == 'testfile'


def test_literal_input(document, comm):

    literal = LiteralInput(value={}, type=dict, name='Literal')

    widget = literal.get_root(document, comm=comm)

    assert isinstance(widget, literal._widget_type)
    assert widget.title == 'Literal'
    assert widget.value == '{}'

    literal._process_events({'value': "{'key': (0, 2)}"})
    assert literal.value == {'key': (0, 2)}
    assert widget.title == 'Literal'

    literal._process_events({'value': "(0, 2)"})
    assert literal.value == {'key': (0, 2)}
    assert widget.title == 'Literal (wrong type)'

    literal._process_events({'value': "invalid"})
    assert literal.value == {'key': (0, 2)}
    assert widget.title == 'Literal (invalid)'

    literal._process_events({'value': "{'key': (0, 3)}"})
    assert literal.value == {'key': (0, 3)}
    assert widget.title == 'Literal'

    with pytest.raises(ValueError):
        literal.value = []

def test_literal_input_inheritance():
    """LiteralInput should be able to be subclassed as done in panel-material-ui."""
    class DictInput(LiteralInput):
        """
        The `DictInput` allows entering a dictionary value using a text input box.
        """

        type = param.ClassSelector(default=dict, class_=(type, tuple))

    with pytest.raises(ValueError) as ex:
        DictInput(value="not a dict")

    assert str(ex.value) == "DictInput expected dict type, but value 'not a dict' is of type str."

def test_static_text(document, comm):

    text = StaticText(value='ABC', name='Text:')

    widget = text.get_root(document, comm=comm)

    assert isinstance(widget, text._widget_type)
    assert widget.text == '<b>Text:</b>: ABC'

    text.value = 'CBA'
    assert widget.text == '<b>Text:</b>: CBA'

    text.value = '<b>Text:</b>: ABC'
    assert widget.text == '<b>Text:</b>: ABC'

def test_static_text_no_sync(document, comm):
    text = StaticText(value='ABC', name='Text:')

    widget = text.get_root(document, comm=comm)

    widget.text = 'CBA'
    assert text.value == 'ABC'

def test_static_text_empty(document, comm):

    text = StaticText(name='Text:')

    widget = text.get_root(document, comm=comm)

    assert widget.text == '<b>Text:</b>: '

def test_static_text_repr(document, comm):

    text = StaticText(value=StaticText, name='Text:')

    widget = text.get_root(document, comm=comm)

    assert widget.text == '<b>Text:</b>: &lt;class &#x27;panel.widgets.input.StaticText&#x27;&gt;'



def test_text_input(document, comm):

    text = TextInput(value='ABC', name='Text:')

    widget = text.get_root(document, comm=comm)

    assert isinstance(widget, text._widget_type)
    assert widget.value == 'ABC'
    assert widget.title == 'Text:'

    text._process_events({'value': 'CBA'})
    assert text.value == 'CBA'

    text.value = 'A'
    assert widget.value == 'A'

def test_datetime_input(document, comm):
    dt_input = DatetimeInput(value=datetime(2018, 1, 1),
                             start=datetime(2017, 12, 31),
                             end=datetime(2018, 1, 10),
                             name='Datetime')

    widget = dt_input.get_root(document, comm=comm)

    assert isinstance(widget, dt_input._widget_type)
    assert widget.title == 'Datetime'
    assert widget.value == '2018-01-01 00:00:00'

    dt_input._process_events({'value': '2018-01-01 00:00:01'})
    assert dt_input.value == datetime(2018, 1, 1, 0, 0, 1)
    assert widget.title == 'Datetime'

    dt_input._process_events({'value': '2018-01-01 00:00:01a'})
    assert dt_input.value == datetime(2018, 1, 1, 0, 0, 1)
    assert widget.title == 'Datetime (invalid)'

    dt_input._process_events({'value': '2018-01-11 00:00:00'})
    assert dt_input.value == datetime(2018, 1, 1, 0, 0, 1)
    assert widget.title == 'Datetime (out of bounds)'

    dt_input._process_events({'value': '2018-01-02 00:00:01'})
    assert dt_input.value == datetime(2018, 1, 2, 0, 0, 1)
    assert widget.title == 'Datetime'

    with pytest.raises(ValueError):
        dt_input.value = datetime(2017, 12, 30)


def test_datetime_range_input(document, comm):
    dt_input = DatetimeRangeInput(
        value=(datetime(2018, 1, 1), datetime(2018, 1, 3)),
        start=datetime(2017, 12, 31), end=datetime(2018, 1, 10),
        name='Datetime')

    composite = dt_input.get_root(document, comm=comm)
    label, start, end = composite.children

    assert isinstance(composite, dt_input._composite_type._bokeh_model)
    assert label.text == 'Datetime'
    assert start.value == '2018-01-01 00:00:00'
    assert end.value == '2018-01-03 00:00:00'

    dt_input._start._process_events({'value': '2018-01-01 00:00:01'})
    assert dt_input.value == (datetime(2018, 1, 1, 0, 0, 1), datetime(2018, 1, 3))
    assert label.text == 'Datetime'

    dt_input._start._process_events({'value': '2018-01-01 00:00:01a'})
    assert dt_input.value == (datetime(2018, 1, 1, 0, 0, 1), datetime(2018, 1, 3))
    assert label.text == 'Datetime (invalid)'

    dt_input._start._process_events({'value': '2018-01-11 00:00:00'})
    assert dt_input.value == (datetime(2018, 1, 1, 0, 0, 1), datetime(2018, 1, 3))
    assert label.text == 'Datetime (out of bounds)'

    dt_input._end._process_events({'value': '2018-01-11 00:00:00a'})
    assert dt_input.value == (datetime(2018, 1, 1, 0, 0, 1), datetime(2018, 1, 3))
    assert label.text == 'Datetime (out of bounds) (invalid)'

    dt_input._start._process_events({'value': '2018-01-02 00:00:00'})
    dt_input._end._process_events({'value': '2018-01-03 00:00:00'})
    assert dt_input.value == (datetime(2018, 1, 2), datetime(2018, 1, 3))
    assert label.text == 'Datetime'

    dt_input._start._process_events({'value': '2018-01-05 00:00:00'})
    assert dt_input.value == (datetime(2018, 1, 2), datetime(2018, 1, 3))
    assert label.text == 'Datetime (start of range must be <= end)'

    dt_input._start._process_events({'value': '2018-01-01 00:00:00'})
    assert dt_input.value == (datetime(2018, 1, 1), datetime(2018, 1, 3))
    assert label.text == 'Datetime'


def test_int_input(document, comm):
    int_input = IntInput(name='Int input')
    widget = int_input.get_root(document, comm=comm)

    assert isinstance(widget, int_input._widget_type)
    assert widget.title == 'Int input'
    assert widget.step == 1
    assert widget.value == 0

    int_input._process_events({'value': 2})
    assert int_input.value == 2
    int_input._process_events({'value_throttled': 2})
    assert int_input.value_throttled == 2

    int_input.value = 0
    assert widget.value == 0

    # Testing throttled mode
    with config.set(throttled=True):
        int_input._process_events({'value': 1})
        assert int_input.value == 0  # no change
        int_input._process_events({'value_throttled': 1})
        assert int_input.value == 1

        int_input.value = 2
        assert widget.value == 2


def test_float_input(document, comm):
    float_input = FloatInput(value=0.4, name="Float input")
    widget = float_input.get_root(document, comm=comm)

    assert isinstance(widget, float_input._widget_type)
    assert widget.title == 'Float input'
    assert widget.step == 0.1
    assert widget.value == 0.4

    float_input._process_events({'value': 0.2})
    assert float_input.value == 0.2
    float_input._process_events({'value_throttled': 0.2})
    assert float_input.value_throttled == 0.2

    float_input.value = 0.3
    assert widget.value == 0.3

    # Testing throttled mode
    with config.set(throttled=True):
        float_input._process_events({'value': 0.4})
        assert float_input.value == 0.3  # no change
        float_input._process_events({'value_throttled': 0.4})
        assert float_input.value == 0.4

        float_input.value = 0.5
        assert widget.value == 0.5


def test_array_input(document, comm):
    array_input = ArrayInput(value=np.array([1, 2, 3]),  name="Array input", max_array_size=3)
    widget = array_input.get_root(document, comm=comm)

    assert isinstance(widget, array_input._widget_type)
    assert widget.title == 'Array input'
    assert widget.value == '[1, 2, 3]'
    assert not widget.disabled

    array_input.value = np.array([1, 2, 3, 4])

    assert widget.value == "'[1,2,3,4]'"
    assert widget.disabled
    assert array_input._auto_disabled

    array_input.value = np.array([1, 2, 3])
    assert not widget.disabled
    assert not array_input._auto_disabled


def test_file_input_save_one_file(document, comm, tmpdir):
    file_input = FileInput(accept='.txt')

    widget = file_input.get_root(document, comm=comm)

    assert isinstance(widget, BkFileInput)

    file_input._process_events({'mime_type': 'text/plain', 'value': 'U29tZSB0ZXh0Cg==', 'filename': 'testfile'})

    fpath = Path(tmpdir) / 'out.txt'
    file_input.save(str(fpath))

    assert fpath.exists()
    content = fpath.read_text()
    assert content == 'Some text\n'
