import itertools
import re
from datetime import datetime, timedelta

import pytest

import icalendar
from icalendar import prop
from icalendar.cal import Calendar, Component, Event
from icalendar.prop import tzid_from_dt


def test_cal_Component(calendar_component):
    """A component is like a dictionary with extra methods and attributes."""
    assert calendar_component
    assert calendar_component.is_empty()


def test_nonempty_calendar_component(calendar_component):
    """Every key defines a property.A property can consist of either a
    single item. This can be set with a single value...
    """
    calendar_component["prodid"] = "-//max m//icalendar.mxm.dk/"
    assert not calendar_component.is_empty()
    assert calendar_component == Calendar({"PRODID": "-//max m//icalendar.mxm.dk/"})

    # or with a list
    calendar_component["ATTENDEE"] = ["Max M", "Rasmussen"]
    assert calendar_component == Calendar(
        {"ATTENDEE": ["Max M", "Rasmussen"], "PRODID": "-//max m//icalendar.mxm.dk/"}
    )


def test_add_multiple_values(event_component):
    """add multiple values to a property.

    If you use the add method you don't have to considder if a value is
    a list or not.
    """
    # add multiple values at once
    event_component.add("attendee", ["test@test.com", "test2@test.com"])

    # or add one per line
    event_component.add("attendee", "maxm@mxm.dk")
    event_component.add("attendee", "test@example.dk")

    # add again multiple values at once to very concatenaton of lists
    event_component.add("attendee", ["test3@test.com", "test4@test.com"])

    assert event_component == Event(
        {
            "ATTENDEE": [
                prop.vCalAddress("test@test.com"),
                prop.vCalAddress("test2@test.com"),
                prop.vCalAddress("maxm@mxm.dk"),
                prop.vCalAddress("test@example.dk"),
                prop.vCalAddress("test3@test.com"),
                prop.vCalAddress("test4@test.com"),
            ]
        }
    )


def test_get_content_directly(c):
    """You can get the values back directly ..."""
    c.add("prodid", "-//my product//")
    assert c["prodid"] == prop.vText("-//my product//")
    # ... or decoded to a python type
    assert c.decoded("prodid") == b"-//my product//"


def test_get_default_value(c):
    """With default values for non existing properties"""
    assert c.decoded("version", "No Version") == "No Version"


def test_default_list_example(c):
    c.add("rdate", [datetime(2013, 3, 28), datetime(2013, 3, 27)])
    assert isinstance(c.decoded("rdate"), prop.vDDDLists)


def test_render_component(calendar_component):
    """The component can render itself in the RFC 5545 format."""
    calendar_component.add("attendee", "Max M")
    assert (
        calendar_component.to_ical()
        == b"BEGIN:VCALENDAR\r\nATTENDEE:Max M\r\nEND:VCALENDAR\r\n"
    )


def test_nested_component_event_ics(filled_event_component):
    """Check the ical string of the event component."""
    assert filled_event_component.to_ical() == (
        b"BEGIN:VEVENT\r\nDTEND:20000102T000000\r\n"
        + b"DTSTART:20000101T000000\r\nSUMMARY:A brief history of time\r"
        + b"\nEND:VEVENT\r\n"
    )


def test_nested_components(calendar_component, filled_event_component):
    """Components can be nested, so You can add a subcomponent. Eg a calendar
    holds events."""
    self.assertEqual(
        calendar_component.subcomponents,
        [
            Event(
                {
                    "DTEND": "20000102T000000",
                    "DTSTART": "20000101T000000",
                    "SUMMARY": "A brief history of time",
                }
            )
        ],
    )


def test_walk_filled_calendar_component(calendar_component, filled_event_component):
    """We can walk over nested componentes with the walk method."""
    assert [i.name for i in calendar_component.walk()] == ["VCALENDAR", "VEVENT"]


def test_filter_walk(calendar_component, filled_event_component):
    """We can also just walk over specific component types, by filtering
    them on their name."""
    assert [i.name for i in calendar_component.walk("VEVENT")] == ["VEVENT"]
    assert [i["dtstart"] for i in calendar_component.walk("VEVENT")] == [
        "20000101T000000"
    ]


def test_recursive_property_items(calendar_component, filled_event_component):
    """We can enumerate property items recursively with the property_items
    method."""
    calendar_component.add("attendee", "Max M")
    assert calendar_component.property_items() == [
        ("BEGIN", b"VCALENDAR"),
        ("ATTENDEE", prop.vCalAddress("Max M")),
        ("BEGIN", b"VEVENT"),
        ("DTEND", "20000102T000000"),
        ("DTSTART", "20000101T000000"),
        ("SUMMARY", "A brief history of time"),
        ("END", b"VEVENT"),
        ("END", b"VCALENDAR"),
    ]


def test_flat_property_items(calendar_component, filled_event_component):
    """We can also enumerate property items just under the component."""
    assert calendar_component.property_items(recursive=False) == [
        ("BEGIN", b"VCALENDAR"),
        ("ATTENDEE", prop.vCalAddress("Max M")),
        ("END", b"VCALENDAR"),
    ]


def test_flat_property_items(filled_event_component):
    """Flat enumeration on the event."""
    assert filled_event_component.property_items(recursive=False) == [
        ("BEGIN", b"VEVENT"),
        ("DTEND", "20000102T000000"),
        ("DTSTART", "20000101T000000"),
        ("SUMMARY", "A brief history of time"),
        ("END", b"VEVENT"),
    ]


def test_indent():
    """Text fields which span multiple mulitple lines require proper indenting"""
    c = Calendar()
    c["description"] = "Paragraph one\n\nParagraph two"
    assert c.to_ical() == (
        b"BEGIN:VCALENDAR\r\nDESCRIPTION:Paragraph one\\n\\nParagraph two"
        + b"\r\nEND:VCALENDAR\r\n"
    )


def test_INLINE_properties(calendar_with_resources):
    """INLINE properties have their values on one property line. Note the
    double quoting of the value with a colon in it.
    """
    assert calendar_with_resources == Calendar(
        {"RESOURCES": 'Chair, Table, "Room: 42"'}
    )
    assert calendar_with_resources.to_ical() == (
        b'BEGIN:VCALENDAR\r\nRESOURCES:Chair\\, Table\\, "Room: 42"\r\n'
        + b"END:VCALENDAR\r\n"
    )


def test_get_inline(calendar_with_resources):
    """The inline values must be handled by the get_inline() and
    set_inline() methods.
    """
    assert calendar_with_resources.get_inline("resources", decode=0) == [
        "Chair",
        "Table",
        "Room: 42",
    ]


def test_get_inline_decoded(calendar_with_resources):
    """These can also be decoded"""
    assert calendar_with_resources.get_inline("resources", decode=1) == [
        b"Chair",
        b"Table",
        b"Room: 42",
    ]


def test_set_inline(calendar_with_resources):
    """You can set them directly ..."""
    calendar_with_resources.set_inline(
        "resources", ["A", "List", "of", "some, recources"], encode=1
    )
    assert calendar_with_resources["resources"] == 'A,List,of,"some, recources"'
    assert calendar_with_resources.get_inline("resources", decode=0) == [
        "A",
        "List",
        "of",
        "some, recources",
    ]


def test_inline_free_busy_inline(c):
    c["freebusy"] = (
        "19970308T160000Z/PT3H,19970308T200000Z/PT1H,"
        + "19970308T230000Z/19970309T000000Z"
    )
    assert c.get_inline("freebusy", decode=0) == [
        "19970308T160000Z/PT3H",
        "19970308T200000Z/PT1H",
        "19970308T230000Z/19970309T000000Z",
    ]

    freebusy = c.get_inline("freebusy", decode=1)
    assert isinstance(freebusy[0][0], datetime)
    assert isinstance(freebusy[0][1], timedelta)


def test_cal_Component_add(comp, tzp):
    """Test the for timezone correctness: dtstart should preserve it's
    timezone, created, dtstamp and last-modified must be in UTC.
    """
    comp.add("dtstart", tzp.localize(datetime(2010, 10, 10, 10, 0, 0), "Europe/Vienna"))
    comp.add("created", datetime(2010, 10, 10, 12, 0, 0))
    comp.add("dtstamp", tzp.localize(datetime(2010, 10, 10, 14, 0, 0), "Europe/Vienna"))
    comp.add("last-modified", tzp.localize_utc(datetime(2010, 10, 10, 16, 0, 0)))

    lines = comp.to_ical().splitlines()
    assert b"DTSTART;TZID=Europe/Vienna:20101010T100000" in lines
    assert b"CREATED:20101010T120000Z" in lines
    assert b"DTSTAMP:20101010T120000Z" in lines
    assert b"LAST-MODIFIED:20101010T160000Z" in lines


def test_cal_Component_add_no_reencode(comp):
    """Already encoded values should not be re-encoded."""
    comp.add("ATTACH", "me")
    comp.add("ATTACH", "you", encode=False)
    binary = prop.vBinary("us")
    comp.add("ATTACH", binary)

    assert comp["ATTACH"] == ["me", "you", binary]


def test_cal_Component_add_property_parameter(comp):
    """Test the for timezone correctness: dtstart should preserve it's
    timezone, crated, dtstamp and last-modified must be in UTC.
    """
    comp.add("X-TEST-PROP", "tryout.", parameters={"prop1": "val1", "prop2": "val2"})
    lines = comp.to_ical().splitlines()
    assert b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines


comp_prop = pytest.mark.parametrize(
    "component_name, property_name",
    [
        ("VEVENT", "DTSTART"),
        ("VEVENT", "DTEND"),
        ("VEVENT", "RECURRENCE-ID"),
        ("VTODO", "DUE"),
    ],
)


@comp_prop
def test_cal_Component_from_ical(component_name, property_name, tzp):
    """Check for proper handling of TZID parameter of datetime properties"""
    component_str = "BEGIN:" + component_name + "\n"
    component_str += property_name + ";TZID=America/Denver:"
    component_str += "20120404T073000\nEND:" + component_name
    component = Component.from_ical(component_str)
    assert tzid_from_dt(component[property_name].dt) == "America/Denver"


@comp_prop
def test_cal_Component_from_ical_2(component_name, property_name, tzp):
    """Check for proper handling of TZID parameter of datetime properties"""
    component_str = "BEGIN:" + component_name + "\n"
    component_str += property_name + ":"
    component_str += "20120404T073000\nEND:" + component_name
    component = Component.from_ical(component_str)
    assert component[property_name].dt.tzinfo == None


def test_cal_Component_to_ical_property_order():
    component_str = [
        b"BEGIN:VEVENT",
        b"DTSTART:19970714T170000Z",
        b"DTEND:19970715T035959Z",
        b"SUMMARY:Bastille Day Party",
        b"END:VEVENT",
    ]
    component = Component.from_ical(b"\r\n".join(component_str))

    sorted_str = component.to_ical().splitlines()
    assert sorted_str != component_str
    assert set(sorted_str) == set(component_str)

    preserved_str = component.to_ical(sorted=False).splitlines()
    assert preserved_str == component_str


def test_cal_Component_to_ical_parameter_order():
    component_str = [
        b"BEGIN:VEVENT",
        b"X-FOOBAR;C=one;A=two;B=three:helloworld.",
        b"END:VEVENT",
    ]
    component = Component.from_ical(b"\r\n".join(component_str))

    sorted_str = component.to_ical().splitlines()
    assert sorted_str[0] == component_str[0]
    assert sorted_str[1] == b"X-FOOBAR;A=two;B=three;C=one:helloworld."
    assert sorted_str[2] == component_str[2]

    preserved_str = component.to_ical(sorted=False).splitlines()
    assert preserved_str == component_str


@pytest.fixture
def repr_example(c):
    class ReprExample:
        component = c
        component["key1"] = "value1"
        calendar = Calendar()
        calendar["key1"] = "value1"
        event = Event()
        event["key1"] = "value1"
        nested = Component(key1="VALUE1")
        nested.add_component(component)
        nested.add_component(calendar)

    return ReprExample


def test_repr_component(repr_example):
    """Test correct class representation."""
    assert re.match(r"Component\({u?'KEY1': u?'value1'}\)", str(repr_example.component))


def test_repr_calendar(repr_example):
    assert re.match(r"VCALENDAR\({u?'KEY1': u?'value1'}\)", str(repr_example.calendar))


def test_repr_event(repr_example):
    assert re.match(r"VEVENT\({u?'KEY1': u?'value1'}\)", str(repr_example.event))


def test_nested_components(repr_example):
    """Representation of nested Components"""
    repr_example.calendar.add_component(repr_example.event)
    print(repr_example.nested)
    assert re.match(
        r"Component\({u?'KEY1': u?'VALUE1'}, "
        r"Component\({u?'KEY1': u?'value1'}\), "
        r"VCALENDAR\({u?'KEY1': u?'value1'}, "
        r"VEVENT\({u?'KEY1': u?'value1'}\)\)\)",
        str(repr_example.nested),
    )


def test_component_factory_VEVENT(factory):
    """Check the events in the component factory"""
    component = factory["VEVENT"]
    event = component(dtstart="19700101")
    assert event.to_ical() == b"BEGIN:VEVENT\r\nDTSTART:19700101\r\nEND:VEVENT\r\n"


def test_component_factory_VCALENDAR(factory):
    """Check the VCALENDAR in the factory."""
    assert factory.get("VCALENDAR") == icalendar.cal.Calendar


def test_minimal_calendar_component_with_one_event():
    """Setting up a minimal calendar component looks like this"""
    cal = Calendar()

    # Some properties are required to be compliant
    cal["prodid"] = "-//My calendar product//mxm.dk//"
    cal["version"] = "2.0"

    # We also need at least one subcomponent for a calendar to be compliant
    event = Event()
    event["summary"] = "Python meeting about calendaring"
    event["uid"] = "42"
    event.add("dtstart", datetime(2005, 4, 4, 8, 0, 0))
    cal.add_component(event)
    assert (
        cal.subcomponents[0].to_ical()
        == b"BEGIN:VEVENT\r\nSUMMARY:Python meeting about calendaring\r\n"
        + b"DTSTART:20050404T080000\r\nUID:42\r\n"
        + b"END:VEVENT\r\n"
    )


def test_calendar_with_parsing_errors_includes_all_events(calendars):
    """Parsing a complete calendar from a string will silently ignore wrong
    events but adding the error information to the component's 'errors'
    attribute. The error in the following is the third EXDATE: it has an
    empty DATE.
    """
    event_descriptions = [
        e["DESCRIPTION"].to_ical() for e in calendars.parsing_error.walk("VEVENT")
    ]
    assert event_descriptions == [b"Perfectly OK event", b"Wrong event"]


def test_calendar_with_parsing_errors_has_an_error_in_one_event(calendars):
    """Parsing a complete calendar from a string will silently ignore wrong
    events but adding the error information to the component's 'errors'
    attribute. The error in the following is the third EXDATE: it has an
    empty DATE.
    """
    errors = [e.errors for e in calendars.parsing_error.walk("VEVENT")]
    assert errors == [[], [("EXDATE", "Expected datetime, date, or time, got: ''")]]


def test_cal_strict_parsing(calendars):
    """If components are damaged, we raise an exception."""
    with pytest.raises(ValueError):
        calendars.parsing_error_in_UTC_offset


def test_cal_ignore_errors_parsing(calendars, vUTCOffset_ignore_exceptions):
    """If we diable the errors, we should be able to put the calendar back together."""
    assert (
        calendars.parsing_error_in_UTC_offset.to_ical()
        == calendars.parsing_error_in_UTC_offset.raw_ics
    )


@pytest.mark.parametrize(
    ("calendar", "other_calendar"),
    itertools.product(
        [
            "issue_156_RDATE_with_PERIOD_TZID_khal",
            "issue_156_RDATE_with_PERIOD_TZID_khal_2",
            "issue_178_custom_component_contains_other",
            "issue_178_custom_component_inside_other",
            "issue_526_calendar_with_events",
            "issue_526_calendar_with_different_events",
            "issue_526_calendar_with_event_subset",
        ],
        repeat=2,
    ),
)
def test_comparing_calendars(calendars, calendar, other_calendar, tzp):
    are_calendars_equal = calendars[calendar] == calendars[other_calendar]
    are_calendars_actually_equal = calendar == other_calendar
    assert are_calendars_equal == are_calendars_actually_equal


@pytest.mark.parametrize(
    ("calendar", "shuffeled_calendar"),
    [
        (
            "issue_526_calendar_with_events",
            "issue_526_calendar_with_shuffeled_events",
        ),
    ],
)
def test_calendars_with_same_subcomponents_in_different_order_are_equal(
    calendars, calendar, shuffeled_calendar
):
    assert (
        calendars[calendar].subcomponents != calendars[shuffeled_calendar].subcomponents
    )
    assert calendars[calendar] == calendars[shuffeled_calendar]
