##
# Copyright (c) 2012-2017 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

from __future__ import print_function
from twext.enterprise.dal.syntax import Update
from twext.enterprise.jobs.jobitem import JobItem
from txdav.common.datastore.sql_tables import schema

"""
Tests for calendarserver.tools.calverify
"""

from calendarserver.tools.calverify import BadDataService, \
    SchedulingMismatchService, DoubleBookingService, DarkPurgeService, \
    EventSplitService, MissingLocationService

from pycalendar.datetime import DateTime

from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks

from twistedcaldav.config import config
from twistedcaldav.ical import normalize_iCalStr
from twistedcaldav.test.util import StoreTestCase

from txdav.common.datastore.test.util import populateCalendarsFrom

from StringIO import StringIO


OK_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:OK
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

# Missing DTSTAMP
BAD1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD1
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

# Bad recurrence
BAD2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD2
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
RRULE:FREQ=DAILY;COUNT=3
SEQUENCE:2
END:VEVENT
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD2
RECURRENCE-ID:20100307T120000Z
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

# Bad recurrence
BAD3_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD2
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
RRULE:FREQ=DAILY;COUNT=3
SEQUENCE:2
END:VEVENT
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD2
RECURRENCE-ID:20100307T120000Z
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

# Missing Organizer
BAD3_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD3
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
RRULE:FREQ=DAILY;COUNT=3
SEQUENCE:2
END:VEVENT
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD3
RECURRENCE-ID:20100307T111500Z
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

# https Organizer
BAD4_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD4
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
ORGANIZER:http://demo.com:8008/principals/__uids__/D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")


# https Attendee
BAD5_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD5
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:http://demo.com:8008/principals/__uids__/D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")


# https Organizer and Attendee
BAD6_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD6
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
ORGANIZER:http://demo.com:8008/principals/__uids__/D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:http://demo.com:8008/principals/__uids__/D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")


# Base64 Organizer and Attendee parameter
OK8_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:OK8
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
ORGANIZER;CALENDARSERVER-OLD-CUA="base64-aHR0cDovL2RlbW8uY29tOjgwMDgvcHJpbm
 NpcGFscy9fX3VpZHNfXy9ENDZGM0Q3MS0wNEI3LTQzQzItQTdCNi02RjkyRjkyRTYxRDA=":
 urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;CALENDARSERVER-OLD-CUA="base64-aHR0cDovL2RlbW8uY29tOjgwMDgvcHJpbmN
 pcGFscy9fX3VpZHNfXy9ENDZGM0Q3MS0wNEI3LTQzQzItQTdCNi02RjkyRjkyRTYxRDA=":u
 rn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")


# Non-mailto: Organizer
BAD10_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD10
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
ORGANIZER;CN=Example User1;SCHEDULE-AGENT=NONE:example1@example.com
ATTENDEE;CN=Example User1:example1@example.com
ATTENDEE;CN=Example User2:example2@example.com
ATTENDEE;CN=Example User3:/principals/users/example3
ATTENDEE;CN=Example User4:http://demo.com:8008/principals/users/example4
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

# Bad recurrence EXDATE
BAD11_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD11
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
EXDATE:20100314T111500Z
RRULE:FREQ=WEEKLY
SEQUENCE:2
END:VEVENT
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD11
RECURRENCE-ID:20100314T111500Z
DTEND:20100314T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100314T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

BAD12_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD12
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
ORGANIZER:mailto:example2@example.com
ATTENDEE:mailto:example1@example.com
ATTENDEE:mailto:example2@example.com
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

BAD13_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD13
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

BAD14_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD14
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
GEO:40.1;40.1
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

CORRUPT14_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:BAD14
DTEND:20100307T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:20100307T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
GEO:40.1
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")


class CalVerifyDataTests(StoreTestCase):
    """
    Tests calverify for iCalendar data problems.
    """

    metadata = {
        "accessMode": "PUBLIC",
        "isScheduleObject": True,
        "scheduleTag": "abc",
        "scheduleEtags": (),
        "hasPrivateComment": False,
    }

    requirements = {
        "home1": {
            "calendar_1": {
                "ok.ics": (OK_ICS, metadata,),
                "bad1.ics": (BAD1_ICS, metadata,),
                "bad2.ics": (BAD2_ICS, metadata,),
                "bad3.ics": (BAD3_ICS, metadata,),
                "bad4.ics": (BAD4_ICS, metadata,),
                "bad5.ics": (BAD5_ICS, metadata,),
                "bad6.ics": (BAD6_ICS, metadata,),
                "ok8.ics": (OK8_ICS, metadata,),
                "bad10.ics": (BAD10_ICS, metadata,),
                "bad11.ics": (BAD11_ICS, metadata,),
                "bad12.ics": (BAD12_ICS, metadata,),
                "bad13.ics": (BAD13_ICS, metadata,),
                "bad14.ics": (BAD14_ICS, metadata,),
            }
        },
    }

    number_to_process = len(requirements["home1"]["calendar_1"])

    @inlineCallbacks
    def populate(self):

        # Need to bypass normal validation inside the store
        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())

        # Have to manually write these into the database
        populateTxn = self.storeUnderTest().newTransaction()
        co = schema.CALENDAR_OBJECT
        yield Update(
            {co.ICALENDAR_TEXT: CORRUPT14_ICS},
            Where=co.RESOURCE_NAME == "bad14.ics",
        ).on(populateTxn)
        yield populateTxn.commit()

        self.notifierFactory.reset()

    def storeUnderTest(self):
        """
        Create and return a L{CalendarStore} for testing.
        """
        return self._sqlCalendarStore

    def verifyResultsByUID(self, results, expected):
        reported = set([(home, uid) for home, uid, _ignore_resid, _ignore_reason in results])
        self.assertEqual(reported, expected)

    @inlineCallbacks
    def test_scanBadData(self):
        """
        CalVerifyService.doScan without fix. Make sure it detects common errors.
        Make sure sync-token is not changed.
        """

        sync_token_old = (yield (yield self.calendarUnderTest()).syncToken())
        yield self.commit()

        options = {
            "ical": True,
            "fix": False,
            "nobase64": False,
            "verbose": False,
            "uid": "",
            "uuid": "",
            "path": "",
            "tzid": "",
        }
        output = StringIO()
        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
        calverify.emailDomain = "example.com"
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], self.number_to_process)
        self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
            ("home1", "BAD1",),
            ("home1", "BAD2",),
            ("home1", "BAD3",),
            ("home1", "BAD4",),
            ("home1", "BAD5",),
            ("home1", "BAD6",),
            ("home1", "BAD10",),
            ("home1", "BAD11",),
            ("home1", "BAD12",),
            ("home1", "BAD13",),
            ("home1", "BAD14",),
        )))

        sync_token_new = (yield (yield self.calendarUnderTest()).syncToken())
        self.assertEqual(sync_token_old, sync_token_new)

    @inlineCallbacks
    def test_fixBadData(self):
        """
        CalVerifyService.doScan with fix. Make sure it detects and fixes as much as it can.
        Make sure sync-token is changed.
        """

        sync_token_old = (yield (yield self.calendarUnderTest()).syncToken())
        yield self.commit()

        options = {
            "ical": True,
            "fix": True,
            "nobase64": False,
            "verbose": False,
            "uid": "",
            "uuid": "",
            "path": "",
            "tzid": "",
        }
        output = StringIO()

        # Do fix
        self.patch(config.Scheduling.Options, "PrincipalHostAliases", "demo.com")
        self.patch(config, "HTTPPort", 8008)
        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
        calverify.emailDomain = "example.com"
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], self.number_to_process)
        self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
            ("home1", "BAD1",),
            ("home1", "BAD2",),
            ("home1", "BAD3",),
            ("home1", "BAD4",),
            ("home1", "BAD5",),
            ("home1", "BAD6",),
            ("home1", "BAD10",),
            ("home1", "BAD11",),
            ("home1", "BAD12",),
            ("home1", "BAD13",),
            ("home1", "BAD14",),
        )))

        # Do scan
        options["fix"] = False
        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
        calverify.emailDomain = "example.com"
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], self.number_to_process)
        self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
            ("home1", "BAD1",),
            ("home1", "BAD13",),
        )))

        sync_token_new = (yield (yield self.calendarUnderTest()).syncToken())
        self.assertNotEqual(sync_token_old, sync_token_new)

        # Make sure mailto: fix results in urn:x-uid value without SCHEDULE-AGENT
        obj = yield self.calendarObjectUnderTest(name="bad10.ics")
        ical = yield obj.component()
        org = ical.getOrganizerProperty()
        self.assertEqual(org.value(), "urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0")
        self.assertFalse(org.hasParameter("SCHEDULE-AGENT"))
        for attendee in ical.getAllAttendeeProperties():
            self.assertTrue(
                attendee.value().startswith("urn:x-uid:") or
                attendee.value().startswith("/principals")
            )

    @inlineCallbacks
    def test_scanBadCuaOnly(self):
        """
        CalVerifyService.doScan without fix for CALENDARSERVER-OLD-CUA only. Make sure it detects
        and fixes as much as it can. Make sure sync-token is not changed.
        """

        sync_token_old = (yield (yield self.calendarUnderTest()).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "fix": False,
            "badcua": True,
            "nobase64": False,
            "verbose": False,
            "uid": "",
            "uuid": "",
            "path": "",
            "tzid": "",
        }
        output = StringIO()
        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
        calverify.emailDomain = "example.com"
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], self.number_to_process)
        self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
            ("home1", "BAD4",),
            ("home1", "BAD5",),
            ("home1", "BAD6",),
            ("home1", "BAD10",),
            ("home1", "BAD12",),
            ("home1", "BAD14",),
        )))

        sync_token_new = (yield (yield self.calendarUnderTest()).syncToken())
        self.assertEqual(sync_token_old, sync_token_new)

    @inlineCallbacks
    def test_fixBadCuaOnly(self):
        """
        CalVerifyService.doScan with fix for CALENDARSERVER-OLD-CUA only. Make sure it detects
        and fixes as much as it can. Make sure sync-token is changed.
        """

        sync_token_old = (yield (yield self.calendarUnderTest()).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "fix": True,
            "badcua": True,
            "nobase64": False,
            "verbose": False,
            "uid": "",
            "uuid": "",
            "path": "",
            "tzid": "",
        }
        output = StringIO()

        # Do fix
        self.patch(config.Scheduling.Options, "PrincipalHostAliases", "demo.com")
        self.patch(config, "HTTPPort", 8008)
        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
        calverify.emailDomain = "example.com"
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], self.number_to_process)
        self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
            ("home1", "BAD4",),
            ("home1", "BAD5",),
            ("home1", "BAD6",),
            ("home1", "BAD10",),
            ("home1", "BAD12",),
            ("home1", "BAD14",),
        )))

        # Do scan
        options["fix"] = False
        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
        calverify.emailDomain = "example.com"
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], self.number_to_process)
        self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
        )))

        sync_token_new = (yield (yield self.calendarUnderTest()).syncToken())
        self.assertNotEqual(sync_token_old, sync_token_new)


class CalVerifyMismatchTestsBase(StoreTestCase):
    """
    Tests calverify for iCalendar mismatch problems.
    """

    metadata = {
        "accessMode": "PUBLIC",
        "isScheduleObject": True,
        "scheduleTag": "abc",
        "scheduleEtags": (),
        "hasPrivateComment": False,
    }

    uuid1 = "D46F3D71-04B7-43C2-A7B6-6F92F92E61D0"
    uuid2 = "47B16BB4-DB5F-4BF6-85FE-A7DA54230F92"
    uuid3 = "AC478592-7783-44D1-B2AE-52359B4E8415"
    uuidl1 = "75EA36BE-F71B-40F9-81F9-CF59BF40CA8F"
    uuidl2 = "CDAF464F-9C77-4F56-A7A6-98E4ED9903D6"

    @inlineCallbacks
    def populate(self):

        # Need to bypass normal validation inside the store
        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
        self.notifierFactory.reset()


now = DateTime.getToday()
now.setDay(1)
now.offsetMonth(2)
nowYear = now.getYear()
nowMonth = now.getMonth()


class CalVerifyMismatchTestsNonRecurring(CalVerifyMismatchTestsBase):
    """
    Tests calverify for iCalendar mismatch problems for non-recurring events.
    """

    # Organizer has event, attendees do not
    MISSING_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISSING_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Attendees have event, organizer does not
    MISSING_ORGANIZER_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISSING_ORGANIZER_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISSING_ORGANIZER_3_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISSING_ORGANIZER_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Attendee partstat mismatch
    MISMATCH_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=DECLINED:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH_ATTENDEE_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH_ATTENDEE_3_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Attendee events outside time range
    MISMATCH2_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH2_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=DECLINED:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH2_ATTENDEE_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH2_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH2_ATTENDEE_3_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH2_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Organizer event outside time range
    MISMATCH_ORGANIZER_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ORGANIZER_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=DECLINED:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear - 1, "month": nowMonth}

    MISMATCH_ORGANIZER_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ORGANIZER_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Attendee uuid3 has event with different organizer
    MISMATCH3_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH3_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH3_ATTENDEE_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH3_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH3_ATTENDEE_3_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH3_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH_ORGANIZER_3_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ORGANIZER_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Attendee uuid3 has event they are not invited to
    MISMATCH2_ORGANIZER_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH2_ORGANIZER_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=DECLINED:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH2_ORGANIZER_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH2_ORGANIZER_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=DECLINED:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH2_ORGANIZER_3_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH2_ORGANIZER_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=DECLINED:urn:x-uid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:AC478592-7783-44D1-B2AE-52359B4E8415
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    requirements = {
        CalVerifyMismatchTestsBase.uuid1: {
            "calendar": {
                "missing_attendee.ics": (MISSING_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched_attendee.ics": (MISMATCH_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched2_attendee.ics": (MISMATCH2_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched3_attendee.ics": (MISMATCH3_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched_organizer.ics": (MISMATCH_ORGANIZER_1_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched2_organizer.ics": (MISMATCH2_ORGANIZER_1_ICS, CalVerifyMismatchTestsBase.metadata,),
            },
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid2: {
            "calendar": {
                "mismatched_attendee.ics": (MISMATCH_ATTENDEE_2_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched2_attendee.ics": (MISMATCH2_ATTENDEE_2_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched3_attendee.ics": (MISMATCH3_ATTENDEE_2_ICS, CalVerifyMismatchTestsBase.metadata,),
                "missing_organizer.ics": (MISSING_ORGANIZER_2_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched_organizer.ics": (MISMATCH_ORGANIZER_2_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched2_organizer.ics": (MISMATCH2_ORGANIZER_2_ICS, CalVerifyMismatchTestsBase.metadata,),
            },
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid3: {
            "calendar": {
                "mismatched_attendee.ics": (MISMATCH_ATTENDEE_3_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched3_attendee.ics": (MISMATCH3_ATTENDEE_3_ICS, CalVerifyMismatchTestsBase.metadata,),
                "missing_organizer.ics": (MISSING_ORGANIZER_3_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched2_organizer.ics": (MISMATCH2_ORGANIZER_3_ICS, CalVerifyMismatchTestsBase.metadata,),
            },
            "calendar2": {
                "mismatched_organizer.ics": (MISMATCH_ORGANIZER_3_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched2_attendee.ics": (MISMATCH2_ATTENDEE_3_ICS, CalVerifyMismatchTestsBase.metadata,),
            },
            "inbox": {},
        },
    }

    @inlineCallbacks
    def setUp(self):
        yield super(CalVerifyMismatchTestsNonRecurring, self).setUp()

        home = (yield self.homeUnderTest(name=self.uuid3))
        calendar = (yield self.calendarUnderTest(name="calendar2", home=self.uuid3))
        yield home.setDefaultCalendar(calendar, "VEVENT")
        yield self.commit()

    @inlineCallbacks
    def test_scanMismatchOnly(self):
        """
        CalVerifyService.doScan without fix for mismatches. Make sure it detects
        as much as it can. Make sure sync-token is not changed.
        """

        sync_token_old1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_old2 = (yield (yield self.calendarUnderTest(home=self.uuid2, name="calendar")).syncToken())
        sync_token_old3 = (yield (yield self.calendarUnderTest(home=self.uuid3, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": True,
            "nobase64": False,
            "fix": False,
            "verbose": False,
            "details": False,
            "uid": "",
            "uuid": "",
            "tzid": "",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
        }
        output = StringIO()
        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 17)
        self.assertEqual(calverify.results["Missing Attendee"], set((
            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuid2,),
            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuid3,),
        )))
        self.assertEqual(calverify.results["Mismatch Attendee"], set((
            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuid2,),
            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuid3,),
            ("MISMATCH2_ATTENDEE_ICS", self.uuid1, self.uuid2,),
            ("MISMATCH2_ATTENDEE_ICS", self.uuid1, self.uuid3,),
            ("MISMATCH3_ATTENDEE_ICS", self.uuid1, self.uuid3,),
        )))
        self.assertEqual(calverify.results["Missing Organizer"], set((
            ("MISSING_ORGANIZER_ICS", self.uuid2, self.uuid1,),
            ("MISSING_ORGANIZER_ICS", self.uuid3, self.uuid1,),
        )))
        self.assertEqual(calverify.results["Mismatch Organizer"], set((
            ("MISMATCH_ORGANIZER_ICS", self.uuid2, self.uuid1,),
            ("MISMATCH_ORGANIZER_ICS", self.uuid3, self.uuid1,),
            ("MISMATCH2_ORGANIZER_ICS", self.uuid3, self.uuid1,),
        )))

        self.assertTrue("Fix change event" not in calverify.results)
        self.assertTrue("Fix add event" not in calverify.results)
        self.assertTrue("Fix add inbox" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)
        self.assertTrue("Fix failures" not in calverify.results)
        self.assertTrue("Auto-Accepts" not in calverify.results)

        sync_token_new1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_new2 = (yield (yield self.calendarUnderTest(home=self.uuid2, name="calendar")).syncToken())
        sync_token_new3 = (yield (yield self.calendarUnderTest(home=self.uuid3, name="calendar")).syncToken())
        self.assertEqual(sync_token_old1, sync_token_new1)
        self.assertEqual(sync_token_old2, sync_token_new2)
        self.assertEqual(sync_token_old3, sync_token_new3)

    @inlineCallbacks
    def test_fixMismatch(self):
        """
        CalVerifyService.doScan with fix for mismatches. Make sure it detects
        and fixes as much as it can. Make sure sync-token is not changed.
        """

        sync_token_old1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_old2 = (yield (yield self.calendarUnderTest(home=self.uuid2, name="calendar")).syncToken())
        sync_token_old3 = (yield (yield self.calendarUnderTest(home=self.uuid3, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": True,
            "nobase64": False,
            "fix": True,
            "verbose": False,
            "details": False,
            "uid": "",
            "uuid": "",
            "tzid": "",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
        }
        output = StringIO()
        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 17)
        self.assertEqual(calverify.results["Missing Attendee"], set((
            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuid2,),
            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuid3,),
        )))
        self.assertEqual(calverify.results["Mismatch Attendee"], set((
            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuid2,),
            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuid3,),
            ("MISMATCH2_ATTENDEE_ICS", self.uuid1, self.uuid2,),
            ("MISMATCH2_ATTENDEE_ICS", self.uuid1, self.uuid3,),
            ("MISMATCH3_ATTENDEE_ICS", self.uuid1, self.uuid3,),
        )))
        self.assertEqual(calverify.results["Missing Organizer"], set((
            ("MISSING_ORGANIZER_ICS", self.uuid2, self.uuid1,),
            ("MISSING_ORGANIZER_ICS", self.uuid3, self.uuid1,),
        )))
        self.assertEqual(calverify.results["Mismatch Organizer"], set((
            ("MISMATCH_ORGANIZER_ICS", self.uuid2, self.uuid1,),
            ("MISMATCH_ORGANIZER_ICS", self.uuid3, self.uuid1,),
            ("MISMATCH2_ORGANIZER_ICS", self.uuid3, self.uuid1,),
        )))

        self.assertEqual(calverify.results["Fix change event"], set((
            (self.uuid2, "calendar", "MISMATCH_ATTENDEE_ICS",),
            (self.uuid3, "calendar", "MISMATCH_ATTENDEE_ICS",),
            (self.uuid2, "calendar", "MISMATCH2_ATTENDEE_ICS",),
            (self.uuid3, "calendar2", "MISMATCH2_ATTENDEE_ICS",),
            (self.uuid3, "calendar", "MISMATCH3_ATTENDEE_ICS",),
            (self.uuid2, "calendar", "MISMATCH_ORGANIZER_ICS",),
            (self.uuid3, "calendar2", "MISMATCH_ORGANIZER_ICS",),
        )))

        self.assertEqual(calverify.results["Fix add event"], set((
            (self.uuid2, "calendar", "MISSING_ATTENDEE_ICS",),
            (self.uuid3, "calendar2", "MISSING_ATTENDEE_ICS",),
        )))

        self.assertEqual(calverify.results["Fix add inbox"], set((
            (self.uuid2, "MISSING_ATTENDEE_ICS",),
            (self.uuid3, "MISSING_ATTENDEE_ICS",),
            (self.uuid2, "MISMATCH_ATTENDEE_ICS",),
            (self.uuid3, "MISMATCH_ATTENDEE_ICS",),
            (self.uuid2, "MISMATCH2_ATTENDEE_ICS",),
            (self.uuid3, "MISMATCH2_ATTENDEE_ICS",),
            (self.uuid3, "MISMATCH3_ATTENDEE_ICS",),
            (self.uuid2, "MISMATCH_ORGANIZER_ICS",),
            (self.uuid3, "MISMATCH_ORGANIZER_ICS",),
        )))

        self.assertEqual(calverify.results["Fix remove"], set((
            (self.uuid2, "calendar", "missing_organizer.ics",),
            (self.uuid3, "calendar", "missing_organizer.ics",),
            (self.uuid3, "calendar", "mismatched2_organizer.ics",),
        )))
        obj = yield self.calendarObjectUnderTest(home=self.uuid2, calendar_name="calendar", name="missing_organizer.ics")
        self.assertEqual(obj, None)
        obj = yield self.calendarObjectUnderTest(home=self.uuid3, calendar_name="calendar", name="missing_organizer.ics")
        self.assertEqual(obj, None)
        obj = yield self.calendarObjectUnderTest(home=self.uuid3, calendar_name="calendar", name="mismatched2_organizer.ics")
        self.assertEqual(obj, None)

        self.assertEqual(calverify.results["Fix failures"], 0)
        self.assertEqual(calverify.results["Auto-Accepts"], [])

        sync_token_new1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_new2 = (yield (yield self.calendarUnderTest(home=self.uuid2, name="calendar")).syncToken())
        sync_token_new3 = (yield (yield self.calendarUnderTest(home=self.uuid3, name="calendar")).syncToken())
        self.assertEqual(sync_token_old1, sync_token_new1)
        self.assertNotEqual(sync_token_old2, sync_token_new2)
        self.assertNotEqual(sync_token_old3, sync_token_new3)

        # Re-scan after changes to make sure there are no errors
        yield self.commit()
        options["fix"] = False
        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 14)
        self.assertTrue("Missing Attendee" not in calverify.results)
        self.assertTrue("Mismatch Attendee" not in calverify.results)
        self.assertTrue("Missing Organizer" not in calverify.results)
        self.assertTrue("Mismatch Organizer" not in calverify.results)
        self.assertTrue("Fix add event" not in calverify.results)
        self.assertTrue("Fix add inbox" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)
        self.assertTrue("Fix failures" not in calverify.results)
        self.assertTrue("Auto-Accepts" not in calverify.results)


class CalVerifyMismatchTestsAutoAccept(CalVerifyMismatchTestsBase):
    """
    Tests calverify for iCalendar mismatch problems for auto-accept attendees.
    """

    # Organizer has event, attendee do not
    MISSING_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISSING_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Attendee partstat mismatch
    MISMATCH_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH_ATTENDEE_L1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    requirements = {
        CalVerifyMismatchTestsBase.uuid1: {
            "calendar": {
                "missing_attendee.ics": (MISSING_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched_attendee.ics": (MISMATCH_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
            },
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid2: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid3: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuidl1: {
            "calendar": {
                "mismatched_attendee.ics": (MISMATCH_ATTENDEE_L1_ICS, CalVerifyMismatchTestsBase.metadata,),
            },
            "inbox": {},
        },
    }

    @inlineCallbacks
    def test_scanMismatchOnly(self):
        """
        CalVerifyService.doScan without fix for mismatches. Make sure it detects
        as much as it can. Make sure sync-token is not changed.
        """

        sync_token_old1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": True,
            "nobase64": False,
            "fix": False,
            "verbose": False,
            "details": False,
            "uid": "",
            "uuid": "",
            "tzid": "",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
        }
        output = StringIO()
        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 3)
        self.assertEqual(calverify.results["Missing Attendee"], set((
            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuidl1,),
        )))
        self.assertEqual(calverify.results["Mismatch Attendee"], set((
            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuidl1,),
        )))
        self.assertTrue("Missing Organizer" not in calverify.results)
        self.assertTrue("Mismatch Organizer" not in calverify.results)

        self.assertTrue("Fix change event" not in calverify.results)
        self.assertTrue("Fix add event" not in calverify.results)
        self.assertTrue("Fix add inbox" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)
        self.assertTrue("Fix failures" not in calverify.results)
        self.assertTrue("Auto-Accepts" not in calverify.results)

        sync_token_new1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertEqual(sync_token_old1, sync_token_new1)
        self.assertEqual(sync_token_oldl1, sync_token_newl1)

    @inlineCallbacks
    def test_fixMismatch(self):
        """
        CalVerifyService.doScan with fix for mismatches. Make sure it detects
        and fixes as much as it can. Make sure sync-token is not changed.
        """

        sync_token_old1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": True,
            "nobase64": False,
            "fix": True,
            "verbose": False,
            "details": False,
            "uid": "",
            "uuid": "",
            "tzid": "",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
        }
        output = StringIO()
        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 3)
        self.assertEqual(calverify.results["Missing Attendee"], set((
            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuidl1,),
        )))
        self.assertEqual(calverify.results["Mismatch Attendee"], set((
            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuidl1,),
        )))
        self.assertTrue("Missing Organizer" not in calverify.results)
        self.assertTrue("Mismatch Organizer" not in calverify.results)

        self.assertEqual(calverify.results["Fix change event"], set((
            (self.uuidl1, "calendar", "MISMATCH_ATTENDEE_ICS",),
        )))

        self.assertEqual(calverify.results["Fix add event"], set((
            (self.uuidl1, "calendar", "MISSING_ATTENDEE_ICS",),
        )))

        self.assertEqual(calverify.results["Fix add inbox"], set((
            (self.uuidl1, "MISSING_ATTENDEE_ICS",),
            (self.uuidl1, "MISMATCH_ATTENDEE_ICS",),
        )))

        self.assertTrue("Fix remove" not in calverify.results)

        self.assertEqual(calverify.results["Fix failures"], 0)
        testResults = sorted(calverify.results["Auto-Accepts"], key=lambda x: x["uid"])
        self.assertEqual(testResults[0]["path"], "/calendars/__uids__/%s/calendar/mismatched_attendee.ics" % self.uuidl1)
        self.assertEqual(testResults[0]["uid"], "MISMATCH_ATTENDEE_ICS")
        self.assertEqual(testResults[0]["start"].getText()[:8], "%(year)s%(month)02d07" % {"year": nowYear, "month": nowMonth})
        self.assertEqual(testResults[1]["uid"], "MISSING_ATTENDEE_ICS")
        self.assertEqual(testResults[1]["start"].getText()[:8], "%(year)s%(month)02d07" % {"year": nowYear, "month": nowMonth})

        sync_token_new1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertEqual(sync_token_old1, sync_token_new1)
        self.assertNotEqual(sync_token_oldl1, sync_token_newl1)

        # Re-scan after changes to make sure there are no errors
        yield self.commit()
        options["fix"] = False
        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 4)
        self.assertTrue("Missing Attendee" not in calverify.results)
        self.assertTrue("Mismatch Attendee" not in calverify.results)
        self.assertTrue("Missing Organizer" not in calverify.results)
        self.assertTrue("Mismatch Organizer" not in calverify.results)
        self.assertTrue("Fix add event" not in calverify.results)
        self.assertTrue("Fix add inbox" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)
        self.assertTrue("Fix failures" not in calverify.results)
        self.assertTrue("Auto-Accepts" not in calverify.results)


class CalVerifyMismatchTestsUUID(CalVerifyMismatchTestsBase):
    """
    Tests calverify for iCalendar mismatch problems for auto-accept attendees.
    """

    # Organizer has event, attendee do not
    MISSING_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISSING_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Attendee partstat mismatch
    MISMATCH_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    MISMATCH_ATTENDEE_L1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:MISMATCH_ATTENDEE_ICS
DTEND:%(year)s%(month)02d07T151500Z
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T111500Z
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE;PARTSTAT=ACCEPTED:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    requirements = {
        CalVerifyMismatchTestsBase.uuid1: {
            "calendar": {
                "missing_attendee.ics": (MISSING_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
                "mismatched_attendee.ics": (MISMATCH_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
            },
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid2: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid3: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuidl1: {
            "calendar": {
                "mismatched_attendee.ics": (MISMATCH_ATTENDEE_L1_ICS, CalVerifyMismatchTestsBase.metadata,),
            },
            "inbox": {},
        },
    }

    @inlineCallbacks
    def test_scanMismatchOnly(self):
        """
        CalVerifyService.doScan without fix for mismatches. Make sure it detects
        as much as it can. Make sure sync-token is not changed.
        """

        sync_token_old1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": True,
            "nobase64": False,
            "fix": False,
            "verbose": False,
            "details": False,
            "uid": "",
            "uuid": CalVerifyMismatchTestsBase.uuidl1,
            "tzid": "",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
        }
        output = StringIO()
        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 2)
        self.assertTrue("Missing Attendee" not in calverify.results)
        self.assertEqual(calverify.results["Mismatch Attendee"], set((
            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuidl1,),
        )))
        self.assertTrue("Missing Organizer" not in calverify.results)
        self.assertTrue("Mismatch Organizer" not in calverify.results)

        self.assertTrue("Fix change event" not in calverify.results)
        self.assertTrue("Fix add event" not in calverify.results)
        self.assertTrue("Fix add inbox" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)
        self.assertTrue("Fix failures" not in calverify.results)
        self.assertTrue("Auto-Accepts" not in calverify.results)

        sync_token_new1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertEqual(sync_token_old1, sync_token_new1)
        self.assertEqual(sync_token_oldl1, sync_token_newl1)

    @inlineCallbacks
    def test_fixMismatch(self):
        """
        CalVerifyService.doScan with fix for mismatches. Make sure it detects
        and fixes as much as it can. Make sure sync-token is not changed.
        """

        sync_token_old1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": True,
            "nobase64": False,
            "fix": True,
            "verbose": False,
            "details": False,
            "uid": "",
            "uuid": CalVerifyMismatchTestsBase.uuidl1,
            "tzid": "",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
        }
        output = StringIO()
        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 2)
        self.assertTrue("Missing Attendee" not in calverify.results)
        self.assertEqual(calverify.results["Mismatch Attendee"], set((
            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuidl1,),
        )))
        self.assertTrue("Missing Organizer" not in calverify.results)
        self.assertTrue("Mismatch Organizer" not in calverify.results)

        self.assertEqual(calverify.results["Fix change event"], set((
            (self.uuidl1, "calendar", "MISMATCH_ATTENDEE_ICS",),
        )))

        self.assertTrue("Fix add event" not in calverify.results)

        self.assertEqual(calverify.results["Fix add inbox"], set((
            (self.uuidl1, "MISMATCH_ATTENDEE_ICS",),
        )))

        self.assertTrue("Fix remove" not in calverify.results)

        self.assertEqual(calverify.results["Fix failures"], 0)
        testResults = sorted(calverify.results["Auto-Accepts"], key=lambda x: x["uid"])
        self.assertEqual(testResults[0]["path"], "/calendars/__uids__/%s/calendar/mismatched_attendee.ics" % self.uuidl1)
        self.assertEqual(testResults[0]["uid"], "MISMATCH_ATTENDEE_ICS")
        self.assertEqual(testResults[0]["start"].getText()[:8], "%(year)s%(month)02d07" % {"year": nowYear, "month": nowMonth})

        sync_token_new1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertEqual(sync_token_old1, sync_token_new1)
        self.assertNotEqual(sync_token_oldl1, sync_token_newl1)

        # Re-scan after changes to make sure there are no errors
        yield self.commit()
        options["fix"] = False
        options["uuid"] = CalVerifyMismatchTestsBase.uuidl1
        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 2)
        self.assertTrue("Missing Attendee" not in calverify.results)
        self.assertTrue("Mismatch Attendee" not in calverify.results)
        self.assertTrue("Missing Organizer" not in calverify.results)
        self.assertTrue("Mismatch Organizer" not in calverify.results)
        self.assertTrue("Fix add event" not in calverify.results)
        self.assertTrue("Fix add inbox" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)
        self.assertTrue("Fix failures" not in calverify.results)
        self.assertTrue("Auto-Accepts" not in calverify.results)


class CalVerifyDoubleBooked(CalVerifyMismatchTestsBase):
    """
    Tests calverify for double-bookings.
    """

    # No overlap
    INVITE_NO_OVERLAP_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP_ICS
DTSTART:%(year)s%(month)02d07T100000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Two overlapping
    INVITE_NO_OVERLAP1_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP1_1_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP1_1_ICS
DTSTART:%(year)s%(month)02d07T110000Z
DURATION:PT2H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    INVITE_NO_OVERLAP1_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP1_2_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP1_2_ICS
DTSTART:%(year)s%(month)02d07T120000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Two overlapping with one transparent
    INVITE_NO_OVERLAP2_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP2_1_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP2_1_ICS
DTSTART:%(year)s%(month)02d07T140000Z
DURATION:PT2H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    INVITE_NO_OVERLAP2_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP2_2_ICS
SUMMARY:INVITE_NO_OVERLAP2_2_ICS
DTSTART:%(year)s%(month)02d07T150000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
TRANSP:TRANSPARENT
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Two overlapping with one cancelled
    INVITE_NO_OVERLAP3_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP3_1_ICS
TRANSP:OPAQUE
SUMMARY:Ancient event
DTSTART:%(year)s%(month)02d07T170000Z
DURATION:PT2H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    INVITE_NO_OVERLAP3_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP3_2_ICS
SUMMARY:INVITE_NO_OVERLAP3_2_ICS
DTSTART:%(year)s%(month)02d07T180000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
STATUS:CANCELLED
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Two overlapping recurring
    INVITE_NO_OVERLAP4_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP4_1_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP4_1_ICS
DTSTART:%(year)s%(month)02d08T120000Z
DURATION:PT2H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
RRULE:FREQ=DAILY;COUNT=3
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    INVITE_NO_OVERLAP4_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP4_2_ICS
SUMMARY:INVITE_NO_OVERLAP4_2_ICS
DTSTART:%(year)s%(month)02d09T120000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
RRULE:FREQ=DAILY;COUNT=2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Two overlapping on one recurrence instance
    INVITE_NO_OVERLAP5_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP5_1_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP5_1_ICS
DTSTART:%(year)s%(month)02d12T120000Z
DURATION:PT2H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
RRULE:FREQ=DAILY;COUNT=3
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    INVITE_NO_OVERLAP5_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP5_2_ICS
SUMMARY:INVITE_NO_OVERLAP5_2_ICS
DTSTART:%(year)s%(month)02d13T140000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
RRULE:FREQ=DAILY;COUNT=2
END:VEVENT
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP5_2_ICS
SUMMARY:INVITE_NO_OVERLAP5_2_ICS
RECURRENCE-ID:%(year)s%(month)02d14T140000Z
DTSTART:%(year)s%(month)02d14T130000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Two not overlapping - one all-day
    INVITE_NO_OVERLAP6_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:America/Los_Angeles
BEGIN:DAYLIGHT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
TZNAME:PDT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
TZNAME:PST
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP6_1_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP6_1_ICS
DTSTART;TZID=America/Los_Angeles:%(year)s%(month)02d20T200000
DURATION:PT2H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    INVITE_NO_OVERLAP6_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP6_2_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP6_2_ICS
DTSTART;VALUE=DATE:%(year)s%(month)02d21
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Two overlapping - same organizer and summary
    INVITE_NO_OVERLAP7_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP7_1_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP7_1_ICS
DTSTART:%(year)s%(month)02d23T110000Z
DURATION:PT2H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    INVITE_NO_OVERLAP7_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_OVERLAP7_2_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_OVERLAP7_1_ICS
DTSTART:%(year)s%(month)02d23T120000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    allEvents = {
        "invite1.ics": (INVITE_NO_OVERLAP_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite2.ics": (INVITE_NO_OVERLAP1_1_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite3.ics": (INVITE_NO_OVERLAP1_2_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite4.ics": (INVITE_NO_OVERLAP2_1_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite5.ics": (INVITE_NO_OVERLAP2_2_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite6.ics": (INVITE_NO_OVERLAP3_1_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite7.ics": (INVITE_NO_OVERLAP3_2_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite8.ics": (INVITE_NO_OVERLAP4_1_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite9.ics": (INVITE_NO_OVERLAP4_2_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite10.ics": (INVITE_NO_OVERLAP5_1_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite11.ics": (INVITE_NO_OVERLAP5_2_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite12.ics": (INVITE_NO_OVERLAP6_1_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite13.ics": (INVITE_NO_OVERLAP6_2_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite14.ics": (INVITE_NO_OVERLAP7_1_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite15.ics": (INVITE_NO_OVERLAP7_2_ICS, CalVerifyMismatchTestsBase.metadata,),
    }

    requirements = {
        CalVerifyMismatchTestsBase.uuid1: {
            "calendar": allEvents,
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid2: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid3: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuidl1: {
            "calendar": allEvents,
            "inbox": {},
        },
    }

    @inlineCallbacks
    def test_scanDoubleBookingOnly(self):
        """
        CalVerifyService.doScan without fix for mismatches. Make sure it detects
        as much as it can. Make sure sync-token is not changed.
        """

        sync_token_old1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": False,
            "nobase64": False,
            "double": True,
            "fix": False,
            "verbose": False,
            "details": False,
            "summary": False,
            "days": 365,
            "uid": "",
            "uuid": self.uuidl1,
            "tzid": "utc",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
        }
        output = StringIO()
        calverify = DoubleBookingService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
        self.assertEqual(
            [(sorted((i.uid1, i.uid2,)), str(i.start),) for i in calverify.results["Double-bookings"]],
            [
                (["INVITE_NO_OVERLAP1_1_ICS", "INVITE_NO_OVERLAP1_2_ICS"], "%(year)s%(month)02d07T120000Z" % {"year": nowYear, "month": nowMonth}),
                (["INVITE_NO_OVERLAP4_1_ICS", "INVITE_NO_OVERLAP4_2_ICS"], "%(year)s%(month)02d09T120000Z" % {"year": nowYear, "month": nowMonth}),
                (["INVITE_NO_OVERLAP4_1_ICS", "INVITE_NO_OVERLAP4_2_ICS"], "%(year)s%(month)02d10T120000Z" % {"year": nowYear, "month": nowMonth}),
                (["INVITE_NO_OVERLAP5_1_ICS", "INVITE_NO_OVERLAP5_2_ICS"], "%(year)s%(month)02d14T130000Z" % {"year": nowYear, "month": nowMonth}),
            ],
        )
        self.assertEqual(calverify.results["Number of double-bookings"], 4)
        self.assertEqual(calverify.results["Number of unique double-bookings"], 3)

        sync_token_new1 = (yield (yield self.calendarUnderTest(home=self.uuid1, name="calendar")).syncToken())
        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertEqual(sync_token_old1, sync_token_new1)
        self.assertEqual(sync_token_oldl1, sync_token_newl1)


class CalVerifyDarkPurge(CalVerifyMismatchTestsBase):
    """
    Tests calverify for events.
    """

    # No organizer
    INVITE_NO_ORGANIZER_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_ORGANIZER_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_ORGANIZER_ICS
DTSTART:%(year)s%(month)02d07T100000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Valid organizer
    INVITE_VALID_ORGANIZER_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_VALID_ORGANIZER_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_VALID_ORGANIZER_ICS
DTSTART:%(year)s%(month)02d08T100000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Invalid organizer #1
    INVITE_INVALID_ORGANIZER_1_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_INVALID_ORGANIZER_1_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_INVALID_ORGANIZER_1_ICS
DTSTART:%(year)s%(month)02d09T100000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0-1
ATTENDEE:urn:x-uid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0-1
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    # Invalid organizer #2
    INVITE_INVALID_ORGANIZER_2_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_INVALID_ORGANIZER_2_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_INVALID_ORGANIZER_2_ICS
DTSTART:%(year)s%(month)02d10T100000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:mailto:foobar@example.com
ATTENDEE:mailto:foobar@example.com
ATTENDEE:urn:x-uid:75EA36BE-F71B-40F9-81F9-CF59BF40CA8F
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % {"year": nowYear, "month": nowMonth}

    allEvents = {
        "invite1.ics": (INVITE_NO_ORGANIZER_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite2.ics": (INVITE_VALID_ORGANIZER_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite3.ics": (INVITE_INVALID_ORGANIZER_1_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite4.ics": (INVITE_INVALID_ORGANIZER_2_ICS, CalVerifyMismatchTestsBase.metadata,),
    }

    requirements = {
        CalVerifyMismatchTestsBase.uuid1: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid2: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid3: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuidl1: {
            "calendar": allEvents,
            "inbox": {},
        },
    }

    @inlineCallbacks
    def test_scanDarkEvents(self):
        """
        CalVerifyService.doScan without fix for dark events. Make sure it detects
        as much as it can. Make sure sync-token is not changed.
        """

        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": False,
            "nobase64": False,
            "double": True,
            "dark-purge": False,
            "fix": False,
            "verbose": False,
            "details": False,
            "summary": False,
            "days": 365,
            "uid": "",
            "uuid": self.uuidl1,
            "tzid": "utc",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
            "no-organizer": False,
            "invalid-organizer": False,
            "disabled-organizer": False,
        }
        output = StringIO()
        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
        self.assertEqual(
            sorted([i.uid for i in calverify.results["Dark Events"]]),
            ["INVITE_INVALID_ORGANIZER_1_ICS", "INVITE_INVALID_ORGANIZER_2_ICS", ]
        )
        self.assertEqual(calverify.results["Number of dark events"], 2)
        self.assertTrue("Fix dark events" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)

        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertEqual(sync_token_oldl1, sync_token_newl1)

    @inlineCallbacks
    def test_fixDarkEvents(self):
        """
        CalVerifyService.doScan with fix for dark events. Make sure it detects
        as much as it can. Make sure sync-token is changed.
        """

        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": False,
            "nobase64": False,
            "double": True,
            "dark-purge": False,
            "fix": True,
            "verbose": False,
            "details": False,
            "summary": False,
            "days": 365,
            "uid": "",
            "uuid": self.uuidl1,
            "tzid": "utc",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
            "no-organizer": False,
            "invalid-organizer": False,
            "disabled-organizer": False,
        }
        output = StringIO()
        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
        self.assertEqual(
            sorted([i.uid for i in calverify.results["Dark Events"]]),
            ["INVITE_INVALID_ORGANIZER_1_ICS", "INVITE_INVALID_ORGANIZER_2_ICS", ]
        )
        self.assertEqual(calverify.results["Number of dark events"], 2)
        self.assertEqual(calverify.results["Fix dark events"], 2)
        self.assertTrue("Fix remove" in calverify.results)

        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertNotEqual(sync_token_oldl1, sync_token_newl1)

        # Re-scan after changes to make sure there are no errors
        yield self.commit()
        options["fix"] = False
        options["uuid"] = self.uuidl1
        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 2)
        self.assertEqual(len(calverify.results["Dark Events"]), 0)
        self.assertTrue("Fix dark events" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)

    @inlineCallbacks
    def test_fixDarkEventsNoOrganizerOnly(self):
        """
        CalVerifyService.doScan with fix for dark events. Make sure it detects
        as much as it can. Make sure sync-token is changed.
        """

        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": False,
            "nobase64": False,
            "double": True,
            "dark-purge": False,
            "fix": True,
            "verbose": False,
            "details": False,
            "summary": False,
            "days": 365,
            "uid": "",
            "uuid": self.uuidl1,
            "tzid": "utc",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
            "no-organizer": True,
            "invalid-organizer": False,
            "disabled-organizer": False,
        }
        output = StringIO()
        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
        self.assertEqual(
            sorted([i.uid for i in calverify.results["Dark Events"]]),
            ["INVITE_NO_ORGANIZER_ICS", ]
        )
        self.assertEqual(calverify.results["Number of dark events"], 1)
        self.assertEqual(calverify.results["Fix dark events"], 1)
        self.assertTrue("Fix remove" in calverify.results)

        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertNotEqual(sync_token_oldl1, sync_token_newl1)

        # Re-scan after changes to make sure there are no errors
        yield self.commit()
        options["fix"] = False
        options["uuid"] = self.uuidl1
        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 3)
        self.assertEqual(len(calverify.results["Dark Events"]), 0)
        self.assertTrue("Fix dark events" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)

    @inlineCallbacks
    def test_fixDarkEventsAllTypes(self):
        """
        CalVerifyService.doScan with fix for dark events. Make sure it detects
        as much as it can. Make sure sync-token is changed.
        """

        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": False,
            "nobase64": False,
            "double": True,
            "dark-purge": False,
            "fix": True,
            "verbose": False,
            "details": False,
            "summary": False,
            "days": 365,
            "uid": "",
            "uuid": self.uuidl1,
            "tzid": "utc",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
            "no-organizer": True,
            "invalid-organizer": True,
            "disabled-organizer": True,
        }
        output = StringIO()
        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
        self.assertEqual(
            sorted([i.uid for i in calverify.results["Dark Events"]]),
            ["INVITE_INVALID_ORGANIZER_1_ICS", "INVITE_INVALID_ORGANIZER_2_ICS", "INVITE_NO_ORGANIZER_ICS", ]
        )
        self.assertEqual(calverify.results["Number of dark events"], 3)
        self.assertEqual(calverify.results["Fix dark events"], 3)
        self.assertTrue("Fix remove" in calverify.results)

        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertNotEqual(sync_token_oldl1, sync_token_newl1)

        # Re-scan after changes to make sure there are no errors
        yield self.commit()
        options["fix"] = False
        options["uuid"] = self.uuidl1
        calverify = DarkPurgeService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], 1)
        self.assertEqual(len(calverify.results["Dark Events"]), 0)
        self.assertTrue("Fix dark events" not in calverify.results)
        self.assertTrue("Fix remove" not in calverify.results)


class CalVerifyEventPurge(CalVerifyMismatchTestsBase):
    """
    Tests calverify for events.
    """

    # No organizer
    NO_ORGANIZER_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVITE_NO_ORGANIZER_ICS
TRANSP:OPAQUE
SUMMARY:INVITE_NO_ORGANIZER_ICS
DTSTART:%(now_fwd10)s
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

    # Valid organizer
    VALID_ORGANIZER_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
BEGIN:VEVENT
UID:INVITE_VALID_ORGANIZER_ICS
DTSTART:%(now)s
DURATION:PT1H
ATTENDEE;PARTSTAT=ACCPETED:urn:x-uid:%(uuid1)s
ATTENDEE;RSVP=TRUE:urn:x-uid:%(uuid2)s
ORGANIZER:urn:x-uid:%(uuid1)s
RRULE:FREQ=DAILY
SUMMARY:INVITE_VALID_ORGANIZER_ICS
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

    # Valid attendee
    VALID_ATTENDEE_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
BEGIN:VEVENT
UID:INVITE_VALID_ORGANIZER_ICS
DTSTART:%(now)s
DURATION:PT1H
ATTENDEE;PARTSTAT=ACCPETED:urn:x-uid:%(uuid1)s
ATTENDEE;RSVP=TRUE:urn:x-uid:%(uuid2)s
ORGANIZER:urn:x-uid:%(uuid1)s
RRULE:FREQ=DAILY
SUMMARY:INVITE_VALID_ORGANIZER_ICS
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

    # Valid organizer
    VALID_ORGANIZER_FUTURE_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
BEGIN:VEVENT
UID:INVITE_VALID_ORGANIZER_ICS
DTSTART:%(now_fwd11)s
DURATION:PT1H
ATTENDEE;PARTSTAT=ACCPETED:urn:x-uid:%(uuid1)s
ATTENDEE;RSVP=TRUE:urn:x-uid:%(uuid2)s
ORGANIZER:urn:x-uid:%(uuid1)s
RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
RRULE:FREQ=DAILY
SEQUENCE:1
SUMMARY:INVITE_VALID_ORGANIZER_ICS
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

    # Valid attendee
    VALID_ATTENDEE_FUTURE_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
BEGIN:VEVENT
UID:INVITE_VALID_ORGANIZER_ICS
DTSTART:%(now_fwd11)s
DURATION:PT1H
ATTENDEE;PARTSTAT=ACCPETED:urn:x-uid:%(uuid1)s
ATTENDEE;RSVP=TRUE:urn:x-uid:%(uuid2)s
ORGANIZER:urn:x-uid:%(uuid1)s
RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
RRULE:FREQ=DAILY
SEQUENCE:1
SUMMARY:INVITE_VALID_ORGANIZER_ICS
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

    # Valid organizer
    VALID_ORGANIZER_PAST_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
BEGIN:VEVENT
UID:%(uid)s
DTSTART:%(now)s
DURATION:PT1H
ATTENDEE;PARTSTAT=ACCPETED:urn:x-uid:%(uuid1)s
ATTENDEE;RSVP=TRUE:urn:x-uid:%(uuid2)s
ORGANIZER:urn:x-uid:%(uuid1)s
RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
RRULE:FREQ=DAILY;UNTIL=%(now_fwd11_1)s
SEQUENCE:1
SUMMARY:INVITE_VALID_ORGANIZER_ICS
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

    # Valid attendee
    VALID_ATTENDEE_PAST_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
BEGIN:VEVENT
UID:%(uid)s
DTSTART:%(now)s
DURATION:PT1H
ATTENDEE;PARTSTAT=ACCPETED:urn:x-uid:%(uuid1)s
ATTENDEE;RSVP=TRUE:urn:x-uid:%(uuid2)s
ORGANIZER:urn:x-uid:%(uuid1)s
RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
RRULE:FREQ=DAILY;UNTIL=%(now_fwd11_1)s
SEQUENCE:1
SUMMARY:INVITE_VALID_ORGANIZER_ICS
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

    # Valid organizer
    VALID_ORGANIZER_OVERRIDE_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
BEGIN:VEVENT
UID:VALID_ORGANIZER_OVERRIDE_ICS
DTSTART:%(now)s
DURATION:PT1H
ATTENDEE;PARTSTAT=ACCPETED:urn:x-uid:%(uuid1)s
ATTENDEE;RSVP=TRUE:urn:x-uid:%(uuid2)s
ORGANIZER:urn:x-uid:%(uuid1)s
RRULE:FREQ=DAILY
SUMMARY:INVITE_VALID_ORGANIZER_ICS
END:VEVENT
BEGIN:VEVENT
UID:VALID_ORGANIZER_OVERRIDE_ICS
RECURRENCE-ID:%(now_fwd11)s
DTSTART:%(now_fwd11)s
DURATION:PT2H
ATTENDEE;PARTSTAT=ACCPETED:urn:x-uid:%(uuid1)s
ATTENDEE;RSVP=TRUE:urn:x-uid:%(uuid2)s
ORGANIZER:urn:x-uid:%(uuid1)s
RRULE:FREQ=DAILY
SUMMARY:INVITE_VALID_ORGANIZER_ICS
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

    @inlineCallbacks
    def setUp(self):

        self.subs = {
            "uuid1": CalVerifyMismatchTestsBase.uuid1,
            "uuid2": CalVerifyMismatchTestsBase.uuid2,
        }

        self.now = DateTime.getNowUTC()
        self.now.setHHMMSS(0, 0, 0)

        self.subs["now"] = self.now

        for i in range(30):
            attrname = "now_back%s" % (i + 1,)
            setattr(self, attrname, self.now.duplicate())
            getattr(self, attrname).offsetDay(-(i + 1))
            self.subs[attrname] = getattr(self, attrname)

            attrname_12h = "now_back%s_12h" % (i + 1,)
            setattr(self, attrname_12h, getattr(self, attrname).duplicate())
            getattr(self, attrname_12h).offsetHours(12)
            self.subs[attrname_12h] = getattr(self, attrname_12h)

            attrname_1 = "now_back%s_1" % (i + 1,)
            setattr(self, attrname_1, getattr(self, attrname).duplicate())
            getattr(self, attrname_1).offsetSeconds(-1)
            self.subs[attrname_1] = getattr(self, attrname_1)

        for i in range(30):
            attrname = "now_fwd%s" % (i + 1,)
            setattr(self, attrname, self.now.duplicate())
            getattr(self, attrname).offsetDay(i + 1)
            self.subs[attrname] = getattr(self, attrname)

            attrname_12h = "now_fwd%s_12h" % (i + 1,)
            setattr(self, attrname_12h, getattr(self, attrname).duplicate())
            getattr(self, attrname_12h).offsetHours(12)
            self.subs[attrname_12h] = getattr(self, attrname_12h)

            attrname_1 = "now_fwd%s_1" % (i + 1,)
            setattr(self, attrname_1, getattr(self, attrname).duplicate())
            getattr(self, attrname_1).offsetSeconds(-1)
            self.subs[attrname_1] = getattr(self, attrname_1)

        self.requirements = {
            CalVerifyMismatchTestsBase.uuid1: {
                "calendar": {
                    "invite1.ics": (self.NO_ORGANIZER_ICS % self.subs, CalVerifyMismatchTestsBase.metadata,),
                    "invite2.ics": (self.VALID_ORGANIZER_ICS % self.subs, CalVerifyMismatchTestsBase.metadata,),
                    "invite3.ics": (self.VALID_ORGANIZER_OVERRIDE_ICS % self.subs, CalVerifyMismatchTestsBase.metadata,),
                },
                "inbox": {},
            },
            CalVerifyMismatchTestsBase.uuid2: {
                "calendar": {
                    "invite2a.ics": (self.VALID_ATTENDEE_ICS % self.subs, CalVerifyMismatchTestsBase.metadata,),
                },
                "inbox": {},
            },
            CalVerifyMismatchTestsBase.uuid3: {
                "calendar": {},
                "inbox": {},
            },
            CalVerifyMismatchTestsBase.uuidl1: {
                "calendar": {},
                "inbox": {},
            },
        }

        yield super(CalVerifyEventPurge, self).setUp()

    @inlineCallbacks
    def test_validSplit(self):
        """
        CalVerifyService.doScan without fix for dark events. Make sure it detects
        as much as it can. Make sure sync-token is not changed.
        """

        options = {
            "nuke": False,
            "missing": False,
            "ical": False,
            "mismatch": False,
            "double": True,
            "dark-purge": False,
            "split": True,
            "path": "/calendars/__uids__/%(uuid1)s/calendar/invite2.ics" % self.subs,
            "rid": "%(now_fwd11)s" % self.subs,
            "summary": False,
        }
        output = StringIO()
        calverify = EventSplitService(self._sqlCalendarStore, options, output, reactor, config)
        oldUID, oldRelatedTo = yield calverify.doAction()

        relsubs = dict(self.subs)
        relsubs["uid"] = oldUID
        relsubs["relID"] = oldRelatedTo

        calendar = yield self.calendarUnderTest(home=CalVerifyMismatchTestsBase.uuid1, name="calendar")
        objs = yield calendar.listObjectResources()
        self.assertEqual(len(objs), 4)
        self.assertTrue("invite2.ics" in objs)
        oldName = filter(lambda x: not x.startswith("invite"), objs)[0]

        obj1 = yield calendar.objectResourceWithName("invite2.ics")
        ical1 = yield obj1.component()
        self.assertEqual(normalize_iCalStr(ical1), self.VALID_ORGANIZER_FUTURE_ICS % relsubs)

        obj2 = yield calendar.objectResourceWithName(oldName)
        ical2 = yield obj2.component()
        self.assertEqual(normalize_iCalStr(ical2), self.VALID_ORGANIZER_PAST_ICS % relsubs)

        calendar = yield self.calendarUnderTest(home=CalVerifyMismatchTestsBase.uuid2, name="calendar")
        objs = yield calendar.listObjectResources()
        self.assertEqual(len(objs), 2)
        self.assertTrue("invite2a.ics" in objs)
        oldName = filter(lambda x: not x.startswith("invite"), objs)[0]

        obj1 = yield calendar.objectResourceWithName("invite2a.ics")
        ical1 = yield obj1.component()
        self.assertEqual(normalize_iCalStr(ical1), self.VALID_ATTENDEE_FUTURE_ICS % relsubs)

        obj2 = yield calendar.objectResourceWithName(oldName)
        ical2 = yield obj2.component()
        self.assertEqual(normalize_iCalStr(ical2), self.VALID_ATTENDEE_PAST_ICS % relsubs)

    @inlineCallbacks
    def test_summary(self):
        """
        CalVerifyService.doScan without fix for dark events. Make sure it detects
        as much as it can. Make sure sync-token is not changed.
        """

        options = {
            "nuke": False,
            "missing": False,
            "ical": False,
            "mismatch": False,
            "double": True,
            "dark-purge": False,
            "split": True,
            "path": "/calendars/__uids__/%(uuid1)s/calendar/invite3.ics" % self.subs,
            "rid": "%(now_fwd11)s" % self.subs,
            "summary": True,
        }
        output = StringIO()
        calverify = EventSplitService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()
        result = output.getvalue().splitlines()
        self.assertTrue("%(now)s" % self.subs in result)
        self.assertTrue("%(now_fwd10)s" % self.subs in result)
        self.assertTrue("%(now_fwd11)s *" % self.subs in result)
        self.assertTrue("%(now_fwd12)s" % self.subs in result)


class CalVerifyMissingLocations(CalVerifyMismatchTestsBase):
    """
    Tests calverify for events.
    """

    subs = {
        "year": nowYear,
        "month": nowMonth,
        "uuid1": CalVerifyMismatchTestsBase.uuid1,
        "uuid2": CalVerifyMismatchTestsBase.uuid2,
        "uuid3": CalVerifyMismatchTestsBase.uuid3,
        "uuidl1": CalVerifyMismatchTestsBase.uuidl1,
        "uuidl2": CalVerifyMismatchTestsBase.uuidl2,
    }

    # Valid event
    VALID_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:VALID_ICS
TRANSP:OPAQUE
SUMMARY:VALID_ICS
DTSTART:%(year)s%(month)02d08T100000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid2)s
ATTENDEE:urn:x-uid:%(uuidl1)s
LOCATION:Room 01
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % subs

    # Valid event
    VALID_MULTI_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:VALID_MULTI_ICS
TRANSP:OPAQUE
SUMMARY:VALID_MULTI_ICS
DTSTART:%(year)s%(month)02d08T100000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid2)s
ATTENDEE:urn:x-uid:%(uuidl1)s
ATTENDEE:urn:x-uid:%(uuidl2)s
LOCATION:Room 01\\;Room 02
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % subs

    # Invalid event
    INVALID_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVALID_ICS
TRANSP:OPAQUE
SUMMARY:INVALID_ICS
DTSTART:%(year)s%(month)02d08T120000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid2)s
ATTENDEE:urn:x-uid:%(uuidl1)s
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % subs

    # Invalid event
    INVALID_MULTI_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:INVALID_MULTI_ICS
TRANSP:OPAQUE
SUMMARY:INVALID_MULTI_ICS
DTSTART:%(year)s%(month)02d08T120000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid2)s
ATTENDEE:urn:x-uid:%(uuidl1)s
ATTENDEE:urn:x-uid:%(uuidl2)s
LOCATION:Room 02
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % subs

    # Room as organizer event
    ROOM_ORGANIZER_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:ROOM_ORGANIZER_ICS
TRANSP:OPAQUE
SUMMARY:ROOM_ORGANIZER_ICS
DTSTART:%(year)s%(month)02d08T120000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:%(uuidl1)s
ATTENDEE:urn:x-uid:%(uuidl1)s
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % subs

    # Room event without organizer
    ROOM_NO_ORGANIZER_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:ROOM_NO_ORGANIZER_ICS
TRANSP:OPAQUE
SUMMARY:ROOM_NO_ORGANIZER_ICS
DTSTART:%(year)s%(month)02d08T120000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % subs

    # Invalid event with no organizer copt
    ROOM_MISSING_ORGANIZER_ICS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//iCal 4.0.1//EN
CALSCALE:GREGORIAN
BEGIN:VEVENT
CREATED:20100303T181216Z
UID:ROOM_MISSING_ORGANIZER_ICS
TRANSP:OPAQUE
SUMMARY:ROOM_MISSING_ORGANIZER_ICS
DTSTART:%(year)s%(month)02d08T120000Z
DURATION:PT1H
DTSTAMP:20100303T181220Z
SEQUENCE:2
ORGANIZER:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid1)s
ATTENDEE:urn:x-uid:%(uuid2)s
ATTENDEE:urn:x-uid:%(uuidl1)s
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % subs

    allEvents = {
        "invite1.ics": (VALID_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite2.ics": (INVALID_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite3.ics": (VALID_MULTI_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite4.ics": (INVALID_MULTI_ICS, CalVerifyMismatchTestsBase.metadata,),
    }
    allEvents_Room1 = {
        "invite1.ics": (VALID_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite2.ics": (INVALID_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite3.ics": (VALID_MULTI_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite4.ics": (INVALID_MULTI_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite5.ics": (ROOM_ORGANIZER_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite6.ics": (ROOM_NO_ORGANIZER_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite7.ics": (ROOM_MISSING_ORGANIZER_ICS, CalVerifyMismatchTestsBase.metadata,),
    }
    allEvents_Room2 = {
        "invite3.ics": (VALID_MULTI_ICS, CalVerifyMismatchTestsBase.metadata,),
        "invite4.ics": (INVALID_MULTI_ICS, CalVerifyMismatchTestsBase.metadata,),
    }

    requirements = {
        CalVerifyMismatchTestsBase.uuid1: {
            "calendar": allEvents,
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid2: {
            "calendar": allEvents,
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuid3: {
            "calendar": {},
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuidl1: {
            "calendar": allEvents_Room1,
            "inbox": {},
        },
        CalVerifyMismatchTestsBase.uuidl2: {
            "calendar": allEvents_Room2,
            "inbox": {},
        },
    }

    badEvents = set(("INVALID_ICS", "INVALID_MULTI_ICS", "ROOM_MISSING_ORGANIZER_ICS", "ROOM_ORGANIZER_ICS",))

    @inlineCallbacks
    def test_scanMissingLocations(self):
        """
        MissingLocationService.doAction without fix for missing locations. Make sure it detects
        as much as it can. Make sure sync-token is not changed.
        """

        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": False,
            "nobase64": False,
            "double": False,
            "dark-purge": False,
            "missing-location": True,
            "fix": False,
            "verbose": False,
            "details": False,
            "summary": False,
            "days": 365,
            "uid": "",
            "uuid": self.uuidl1,
            "tzid": "utc",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
            "no-organizer": False,
            "invalid-organizer": False,
            "disabled-organizer": False,
        }
        output = StringIO()
        calverify = MissingLocationService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
        self.assertEqual(
            set([i.uid for i in calverify.results["Bad Events"]]),
            self.badEvents,
        )
        self.assertEqual(calverify.results["Number of bad events"], len(self.badEvents))
        self.assertTrue("Fix bad events" not in calverify.results)

        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertEqual(sync_token_oldl1, sync_token_newl1)

    @inlineCallbacks
    def test_fixMissingLocations(self):
        """
        MissingLocationService.doAction with fix for missing locations. Make sure it detects
        as much as it can. Make sure sync-token is changed.
        """

        # Make sure location is in each users event
        for uid in (self.uuid1, self.uuid2, self.uuidl1,):
            calobj = yield self.calendarObjectUnderTest(home=uid, calendar_name="calendar", name="invite2.ics")
            caldata = yield calobj.componentForUser()
            self.assertTrue("LOCATION:" not in str(caldata))
        yield self.commit()

        sync_token_oldl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        yield self.commit()

        options = {
            "ical": False,
            "badcua": False,
            "mismatch": False,
            "nobase64": False,
            "double": False,
            "dark-purge": False,
            "missing-location": True,
            "fix": True,
            "verbose": False,
            "details": False,
            "summary": False,
            "days": 365,
            "uid": "",
            "uuid": self.uuidl1,
            "tzid": "utc",
            "start": DateTime(nowYear, 1, 1, 0, 0, 0),
            "no-organizer": False,
            "invalid-organizer": False,
            "disabled-organizer": False,
        }
        output = StringIO()
        calverify = MissingLocationService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]))
        self.assertEqual(
            set([i.uid for i in calverify.results["Bad Events"]]),
            self.badEvents,
        )
        self.assertEqual(calverify.results["Number of bad events"], len(self.badEvents))
        self.assertEqual(calverify.results["Fix bad events"], len(self.badEvents))

        sync_token_newl1 = (yield (yield self.calendarUnderTest(home=self.uuidl1, name="calendar")).syncToken())
        self.assertNotEqual(sync_token_oldl1, sync_token_newl1)
        yield self.commit()

        # Wait for it to complete
        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)

        # Re-scan after changes to make sure there are no errors
        options["fix"] = False
        options["uuid"] = self.uuidl1
        calverify = MissingLocationService(self._sqlCalendarStore, options, output, reactor, config)
        yield calverify.doAction()

        self.assertEqual(calverify.results["Number of events to process"], len(self.requirements[CalVerifyMismatchTestsBase.uuidl1]["calendar"]) - 2)
        self.assertEqual(len(calverify.results["Bad Events"]), 0)
        self.assertTrue("Fix bad events" not in calverify.results)

        # Make sure location is in each users event
        for uid in (self.uuid1, self.uuid2, self.uuidl1,):
            calobj = yield self.calendarObjectUnderTest(home=uid, calendar_name="calendar", name="invite2.ics")
            caldata = yield calobj.componentForUser()
            self.assertTrue("LOCATION:" in str(caldata))

        calobj = yield self.calendarObjectUnderTest(home=self.uuidl1, calendar_name="calendar", name="invite3.ics")
        self.assertTrue(calobj is not None)

        calobj = yield self.calendarObjectUnderTest(home=self.uuidl2, calendar_name="calendar", name="invite3.ics")
        self.assertTrue(calobj is not None)

        calobj = yield self.calendarObjectUnderTest(home=self.uuidl1, calendar_name="calendar", name="invite4.ics")
        self.assertTrue(calobj is None)

        calobj = yield self.calendarObjectUnderTest(home=self.uuidl2, calendar_name="calendar", name="invite4.ics")
        self.assertTrue(calobj is not None)

        calobj = yield self.calendarObjectUnderTest(home=self.uuidl1, calendar_name="calendar", name="invite5.ics")
        caldata = yield calobj.componentForUser()
        self.assertTrue("LOCATION:" in str(caldata))

        calobj = yield self.calendarObjectUnderTest(home=self.uuidl1, calendar_name="calendar", name="invite6.ics")
        caldata = yield calobj.componentForUser()
        self.assertTrue("LOCATION:" not in str(caldata))

        calobj = yield self.calendarObjectUnderTest(home=self.uuidl1, calendar_name="calendar", name="invite7.ics")
        self.assertTrue(calobj is None)

        yield self.commit()
