from datetime import datetime, timedelta

import pytest
from dateutil.tz import tzutc
from pytest import fail

from backup.model import GenConfig, GenerationalScheme, DummyBackup, Backup
from backup.time import Time


def test_timezone(time) -> None:
    assert time.local_tz is not None


def test_trivial(time) -> None:
    config = GenConfig(days=1)

    scheme = GenerationalScheme(time, config, count=0)

    backups = [
        makeBackup("single", time.local(1928, 12, 6))
    ]

    assert scheme.getOldest(backups)[1].date() == time.local(1928, 12, 6)


def test_trivial_empty(time):
    config = GenConfig(days=1)
    scheme = GenerationalScheme(time, config, count=0)
    assert scheme.getOldest([])[1] is None


def test_trivial_oldest(time: Time) -> None:
    config = GenConfig(days=1)
    scheme = GenerationalScheme(time, config, count=0)

    backups = [
        makeBackup("test", time.local(1985, 12, 6, 10)),
        makeBackup("test", time.local(1985, 12, 6, 12)),
        makeBackup("test", time.local(1985, 12, 6, 13))
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(1985, 12, 6, 10),
        time.local(1985, 12, 6, 12),
        time.local(1985, 12, 6, 13)
    ])


def test_duplicate_weeks(time):
    config = GenConfig(weeks=1, day_of_week='wed')

    scheme = GenerationalScheme(time, config, count=0)

    backups = [
        makeBackup("test", time.local(1985, 12, 5)),
        makeBackup("test", time.local(1985, 12, 4)),
        makeBackup("test", time.local(1985, 12, 1)),
        makeBackup("test", time.local(1985, 12, 2))
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(1985, 12, 1),
        time.local(1985, 12, 2),
        time.local(1985, 12, 5),
        time.local(1985, 12, 4)
    ])


def test_duplicate_months(time) -> None:
    config = GenConfig(months=2, day_of_month=15)

    scheme = GenerationalScheme(time, config, count=0)

    backups = [
        makeBackup("test", time.local(1985, 12, 6)),
        makeBackup("test", time.local(1985, 12, 15)),
        makeBackup("test", time.local(1985, 11, 20)),
        makeBackup("test", time.local(1985, 11, 15))
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(1985, 11, 20),
        time.local(1985, 12, 6),
        time.local(1985, 11, 15),
        time.local(1985, 12, 15)
    ])


def test_duplicate_years(time):
    config = GenConfig(years=2, day_of_year=1)

    scheme = GenerationalScheme(time, config, count=0)

    backups = [
        makeBackup("test", time.local(1985, 12, 31)),
        makeBackup("test", time.local(1985, 1, 1)),
        makeBackup("test", time.local(1984, 12, 31)),
        makeBackup("test", time.local(1984, 1, 1))
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(1984, 12, 31),
        time.local(1985, 12, 31),
        time.local(1984, 1, 1),
        time.local(1985, 1, 1)
    ])


def test_removal_order(time) -> None:
    config = GenConfig(days=5, weeks=2, months=2, years=2,
                       day_of_week='mon', day_of_month=15, day_of_year=1)

    scheme = GenerationalScheme(time, config, count=0)

    backups = [
        # 5 days, week 1
        makeBackup("test", time.local(1985, 12, 7)),  # day 1
        makeBackup("test", time.local(1985, 12, 6)),  # day 2
        makeBackup("test", time.local(1985, 12, 5)),  # day 3
        makeBackup("test", time.local(1985, 12, 4)),  # day 4
        makeBackup("test", time.local(1985, 12, 3)),  # day 5

        makeBackup("test", time.local(1985, 12, 1)),  # 1st week pref

        # week 2
        makeBackup("test", time.local(1985, 11, 25)),  # 1st month pref

        # month2
        makeBackup("test", time.local(1985, 11, 15)),  # 2nd month pref

        # year 1
        makeBackup("test", time.local(1985, 1, 1)),  # 1st year preference
        makeBackup("test", time.local(1985, 1, 2)),

        # year 2
        makeBackup("test", time.local(1984, 6, 1)),  # 2nd year pref
        makeBackup("test", time.local(1984, 7, 1)),

        # year 3
        makeBackup("test", time.local(1983, 1, 1)),
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(1983, 1, 1),
        time.local(1984, 7, 1),
        time.local(1985, 1, 2),

        time.local(1984, 6, 1),
        time.local(1985, 1, 1),
        time.local(1985, 11, 15),
        time.local(1985, 11, 25),
        time.local(1985, 12, 1),
        time.local(1985, 12, 3),
        time.local(1985, 12, 4),
        time.local(1985, 12, 5),
        time.local(1985, 12, 6),
        time.local(1985, 12, 7)
    ])


@pytest.mark.timeout(60)
def test_simulate_daily_backup_for_4_years(time):
    config = GenConfig(days=4, weeks=4, months=4, years=4,
                       day_of_week='mon', day_of_month=1, day_of_year=1)
    scheme = GenerationalScheme(time, config, count=16)
    backups = simulate(time.local(2019, 1, 1),
                       time.local(2022, 12, 31),
                       scheme)
    assertRemovalOrder(GenerationalScheme(time, config, count=0), backups, [
        # 4 years
        time.local(2019, 1, 1),
        time.local(2020, 1, 1),
        time.local(2021, 1, 1),
        time.local(2022, 1, 1),

        # 4 months
        time.local(2022, 9, 1),
        time.local(2022, 10, 1),
        time.local(2022, 11, 1),
        time.local(2022, 12, 1),

        # 4 weeks
        time.local(2022, 12, 5),
        time.local(2022, 12, 12),
        time.local(2022, 12, 19),
        time.local(2022, 12, 26),

        # 4 days
        time.local(2022, 12, 28),
        time.local(2022, 12, 29),
        time.local(2022, 12, 30),
        time.local(2022, 12, 31)
    ])


@pytest.mark.timeout(60)
def test_simulate_agressive_daily_backup_for_4_years(time):
    config = GenConfig(days=4, weeks=4, months=4, years=4,
                       day_of_week='mon', day_of_month=1, day_of_year=1, aggressive=True)
    scheme = GenerationalScheme(time, config, count=16)
    backups = simulate(time.local(2019, 1, 1),
                       time.local(2022, 12, 31),
                       scheme)

    assertRemovalOrder(GenerationalScheme(time, config, count=0), backups, [
        # 4 years
        time.local(2019, 1, 1),
        time.local(2020, 1, 1),
        time.local(2021, 1, 1),
        time.local(2022, 1, 1),

        # 4 months
        time.local(2022, 9, 1),
        time.local(2022, 10, 1),
        time.local(2022, 11, 1),
        time.local(2022, 12, 1),

        # 4 weeks
        time.local(2022, 12, 5),
        time.local(2022, 12, 12),
        time.local(2022, 12, 19),
        time.local(2022, 12, 26),

        # 4 days
        time.local(2022, 12, 28),
        time.local(2022, 12, 29),
        time.local(2022, 12, 30),
        time.local(2022, 12, 31),
    ])


def test_count_limit(time):
    config = GenConfig(years=2, day_of_year=1)
    scheme = GenerationalScheme(time, config, count=1)
    backups = [
        makeBackup("test", time.local(1985, 1, 1)),
        makeBackup("test", time.local(1984, 1, 1))
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(1984, 1, 1)
    ])


def test_aggressive_removal_below_limit(time):
    config = GenConfig(years=2, day_of_year=1, aggressive=True)
    scheme = GenerationalScheme(time, config, count=5)
    backups = [
        makeBackup("test", time.local(1985, 1, 1)),
        makeBackup("test", time.local(1985, 1, 2))
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(1985, 1, 2)
    ])


def test_aggressive_removal_at_limit_ok(time):
    config = GenConfig(years=2, day_of_year=1, aggressive=True)
    scheme = GenerationalScheme(time, config, count=2)
    backups = [
        makeBackup("test", time.local(1985, 1, 1)),
        makeBackup("test", time.local(1984, 1, 1))
    ]
    assertRemovalOrder(scheme, backups, [])


def test_aggressive_removal_over_limit(time):
    config = GenConfig(years=2, day_of_year=1, aggressive=True)
    scheme = GenerationalScheme(time, config, count=2)
    backups = [
        makeBackup("test", time.local(1985, 1, 1)),
        makeBackup("test", time.local(1984, 1, 1)),
        makeBackup("test", time.local(1983, 1, 1)),
        makeBackup("test", time.local(1983, 1, 2))
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(1983, 1, 1),
        time.local(1983, 1, 2)
    ])


def test_removal_order_week(time: Time):
    config = GenConfig(weeks=1, day_of_week='wed', aggressive=True)
    scheme = GenerationalScheme(time, config, count=1)
    backups = [
        makeBackup("test", time.local(2019, 10, 28)),
        makeBackup("test", time.local(2019, 10, 29)),
        makeBackup("test", time.local(2019, 10, 30, 1)),
        makeBackup("test", time.local(2019, 10, 30, 2)),
        makeBackup("test", time.local(2019, 10, 31)),
        makeBackup("test", time.local(2019, 11, 1)),
        makeBackup("test", time.local(2019, 11, 2)),
        makeBackup("test", time.local(2019, 11, 3)),
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(2019, 10, 28),
        time.local(2019, 10, 29),
        time.local(2019, 10, 30, 1),
        time.local(2019, 10, 31),
        time.local(2019, 11, 1),
        time.local(2019, 11, 2),
        time.local(2019, 11, 3)
    ])


def test_removal_order_month(time):
    config = GenConfig(months=1, day_of_month=20, aggressive=True)

    scheme = GenerationalScheme(time, config, count=1)

    backups = [
        makeBackup("test", time.local(2019, 1, 1)),
        makeBackup("test", time.local(2019, 1, 2)),
        makeBackup("test", time.local(2019, 1, 20, 1)),
        makeBackup("test", time.local(2019, 1, 20, 2)),
        makeBackup("test", time.local(2019, 1, 21)),
        makeBackup("test", time.local(2019, 1, 25)),
        makeBackup("test", time.local(2019, 1, 26)),
        makeBackup("test", time.local(2019, 1, 27)),
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(2019, 1, 1),
        time.local(2019, 1, 2),
        time.local(2019, 1, 20, 1),
        time.local(2019, 1, 21),
        time.local(2019, 1, 25),
        time.local(2019, 1, 26),
        time.local(2019, 1, 27)
    ])


def test_removal_order_many_months(time):
    config = GenConfig(months=70, day_of_month=20, aggressive=True)

    scheme = GenerationalScheme(time, config, count=10)

    backups = [
        makeBackup("test", time.local(2019, 7, 20)),  # preferred
        makeBackup("test", time.local(2018, 7, 18)),  # preferred
        makeBackup("test", time.local(2018, 7, 21)),
        makeBackup("test", time.local(2017, 1, 19)),
        makeBackup("test", time.local(2017, 1, 20)),  # preferred
        makeBackup("test", time.local(2017, 1, 31)),
        makeBackup("test", time.local(2016, 12, 1)),  # preferred
        makeBackup("test", time.local(2014, 1, 31)),
        makeBackup("test", time.local(2014, 1, 1)),  # preferred
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(2014, 1, 31),
        time.local(2017, 1, 19),
        time.local(2017, 1, 31),
        time.local(2018, 7, 21),
    ])


def test_removal_order_years(time):
    config = GenConfig(years=2, day_of_year=15, aggressive=True)

    scheme = GenerationalScheme(time, config, count=10)

    backups = [
        makeBackup("test", time.local(2019, 2, 15)),
        makeBackup("test", time.local(2019, 1, 15)),  # keep
        makeBackup("test", time.local(2018, 1, 14)),
        makeBackup("test", time.local(2018, 1, 15)),  # keep
        makeBackup("test", time.local(2018, 1, 16)),
        makeBackup("test", time.local(2017, 1, 15)),
    ]
    assertRemovalOrder(scheme, backups, [
        time.local(2017, 1, 15),
        time.local(2018, 1, 14),
        time.local(2018, 1, 16),
        time.local(2019, 2, 15),
    ])


@pytest.mark.asyncio
async def test_ignored_generational_labels(time):
    config = GenConfig(days=2)

    scheme = GenerationalScheme(time, config, count=10)
    backup1 = makeBackup("test", time.local(2019, 2, 15))
    backup2 = makeBackup("test", time.local(2019, 2, 14))
    backup3 = makeBackup("test", time.local(2019, 2, 13), ignore=True)
    backups = [backup1, backup2, backup3]
    scheme.handleNaming(backups)
    assert backup1.getStatusDetail() == ['Day 1 of 2']
    assert backup2.getStatusDetail() == ['Day 2 of 2']
    assert backup3.getStatusDetail() is None


def getRemovalOrder(scheme, toCheck):
    backups = list(toCheck)
    removed = []
    while True:
        oldest = scheme.getOldest(backups)
        if not oldest:
            break
        removed.append(oldest.date())
        backups.remove(oldest)
    return removed


def assertRemovalOrder(scheme, toCheck, expected):
    backups = list(toCheck)
    removed = []
    index = 0
    time = scheme.time
    while True:
        reason, oldest = scheme.getOldest(backups)
        if index >= len(expected):
            if oldest is not None:
                fail("at index {0}, expected 'None' but got {1}".format(
                    index, time.toLocal(oldest.date())))
            break
        if oldest.date() != expected[index]:
            fail("at index {0}, expected {1} but got {2}".format(
                index, time.toLocal(expected[index]), time.toLocal(oldest.date())))
        removed.append(oldest.date())
        backups.remove(oldest)
        index += 1
    return removed


def makeBackup(slug, date, name=None, ignore=False) -> Backup:
    if not name:
        name = slug
    return DummyBackup(name, date.astimezone(tzutc()), "src", slug, ignore=ignore)


def simulate(start: datetime, end: datetime, scheme: GenerationalScheme, backups=[]):
    today = start
    while today <= end:
        backups.append(makeBackup("test", today))
        test = scheme.getOldest(backups)
        if test is None:
            pass
        reason, oldest = test
        while oldest is not None:
            backups.remove(oldest)
            test = scheme.getOldest(backups)
            if test is None:
                pass
            reason, oldest = test
        today = today + timedelta(hours=27)
        today = scheme.time.local(today.year, today.month, today.day)
    return backups
