from datetime import timedelta

import time_machine
from django.utils import timezone
from drinks.models import Drink
from lifeevents.models import LifeEvent
from scrobbles.models import Scrobble
from trends.trends.fasting import _classify_gap, compute_fasting


def _dt(days_ago, hour=12, minute=0):
    now = timezone.now()
    return (now - timedelta(days=days_ago)).replace(
        hour=hour, minute=minute, second=0, microsecond=0
    )


def _make_food(user, food, ts):
    return Scrobble.objects.create(
        user=user,
        food=food,
        media_type=Scrobble.MediaType.FOOD,
        timestamp=ts,
        played_to_completion=True,
    )


def _make_drink(user, obj, ts, media_type=Scrobble.MediaType.DRINK):
    kwargs = {}
    if media_type == Scrobble.MediaType.DRINK:
        kwargs["drink"] = obj
    elif media_type == Scrobble.MediaType.BEER:
        kwargs["beer"] = obj
    return Scrobble.objects.create(
        user=user,
        media_type=media_type,
        timestamp=ts,
        played_to_completion=True,
        **kwargs,
    )


class TestClassifyGap:
    def test_below_periodic_is_normal(self):
        assert _classify_gap(10, 16, 24) == "normal"

    def test_at_periodic_threshold(self):
        assert _classify_gap(16, 16, 24) == "periodic"

    def test_between_periodic_and_full(self):
        assert _classify_gap(19.3, 16, 24) == "periodic"

    def test_at_full_threshold(self):
        assert _classify_gap(24, 16, 24) == "full"

    def test_above_full_threshold(self):
        assert _classify_gap(30, 16, 24) == "full"

    def test_just_below_periodic(self):
        assert _classify_gap(15.9, 16, 24) == "normal"


class TestFastingComputation:
    def test_no_food_scrobbles(self, user, food):
        result = compute_fasting(user, period="last_30")
        assert result["fasting_days_count"] == 0
        assert result["fasting_days"] == []

    def test_single_food_scrobble(self, user, food):
        _make_food(user, food, _dt(10))
        result = compute_fasting(user, period="last_30")
        assert result["fasting_days_count"] == 0

    def test_short_gap_not_fasting(self, user, food):
        _make_food(user, food, _dt(5, 8, 0))
        _make_food(user, food, _dt(5, 20, 0))
        result = compute_fasting(user, period="last_30")
        assert result["fasting_days_count"] == 0
        assert result["periodic_fasting_count"] == 0

    def test_periodic_fast_consecutive_days(self, user, food):
        _make_food(user, food, _dt(3, 18, 46))
        _make_food(user, food, _dt(2, 14, 5))
        result = compute_fasting(user, period="last_30")
        assert result["periodic_fasting_count"] == 1
        assert result["full_fasting_count"] == 0
        assert result["fasting_days"][0]["type"] == "periodic"

    def test_periodic_fast_with_gap_day_boundary(
        self, user_with_custom_thresholds, food
    ):
        u = user_with_custom_thresholds
        _make_food(u, food, _dt(3, 18, 0))
        _make_food(u, food, _dt(2, 8, 0))
        result = compute_fasting(u, period="last_30")
        fasting_dates = [d["date"] for d in result["fasting_days"]]
        assert _dt(2).strftime("%Y-%m-%d") in fasting_dates
        assert result["periodic_fasting_count"] >= 1

    def test_full_fast_one_day_gap(self, user, food):
        _make_food(user, food, _dt(5, 12, 0))
        _make_food(user, food, _dt(3, 12, 0))
        result = compute_fasting(user, period="last_30")
        assert result["full_fasting_count"] >= 1
        fasting_dates = [d["date"] for d in result["fasting_days"]]
        assert _dt(4).strftime("%Y-%m-%d") in fasting_dates

    def test_full_fast_multi_day_gap(self, user, food):
        _make_food(user, food, _dt(7, 12, 0))
        _make_food(user, food, _dt(4, 12, 0))
        result = compute_fasting(user, period="last_30")
        assert result["full_fasting_count"] == 2
        fasting_dates = [d["date"] for d in result["fasting_days"]]
        for days_ago in [6, 5]:
            assert _dt(days_ago).strftime("%Y-%m-%d") in fasting_dates

    def test_liquid_fast_with_drink(self, user, food, drink):
        _make_food(user, food, _dt(5, 12, 0))
        _make_food(user, food, _dt(3, 12, 0))
        _make_drink(user, drink, _dt(4, 15, 0))
        result = compute_fasting(user, period="last_30")
        assert result["liquid_fasting_count"] == 1
        day4 = next(
            d
            for d in result["fasting_days"]
            if d["date"] == _dt(4).strftime("%Y-%m-%d")
        )
        assert day4["had_drink"] is True

    def test_full_fast_without_drink(self, user, food):
        _make_food(user, food, _dt(5, 12, 0))
        _make_food(user, food, _dt(3, 12, 0))
        result = compute_fasting(user, period="last_30")
        assert result["liquid_fasting_count"] == 0
        day4 = next(
            d
            for d in result["fasting_days"]
            if d["date"] == _dt(4).strftime("%Y-%m-%d")
        )
        assert day4["had_drink"] is False

    def test_liquid_fast_with_beer(self, user, food, beer):
        _make_food(user, food, _dt(5, 12, 0))
        _make_food(user, food, _dt(3, 12, 0))
        _make_drink(user, beer, _dt(4, 20, 0), Scrobble.MediaType.BEER)
        result = compute_fasting(user, period="last_30")
        assert result["liquid_fasting_count"] == 1

    def test_liquid_fast_only_in_full_fast_window(self, user, food, drink):
        _make_food(user, food, _dt(5, 18, 0))
        _make_food(user, food, _dt(4, 10, 0))
        _make_food(user, food, _dt(3, 12, 0))
        _make_drink(user, drink, _dt(4, 15, 0))
        result = compute_fasting(user, period="last_30")
        for d in result["fasting_days"]:
            if d["type"] == "periodic":
                assert d["had_drink"] is False

    def test_full_fast_overrides_periodic(self, user, food):
        _make_food(user, food, _dt(7, 12, 0))
        _make_food(user, food, _dt(5, 12, 0))
        _make_food(user, food, _dt(4, 12, 0))
        result = compute_fasting(user, period="last_30")
        fasting_dates = {d["date"]: d["type"] for d in result["fasting_days"]}
        day6 = _dt(6).strftime("%Y-%m-%d")
        assert fasting_dates.get(day6) == "full"

    def test_vacation_exemption(self, user, food):
        _make_food(user, food, _dt(10, 12, 0))
        _make_food(user, food, _dt(8, 12, 0))
        life_event = LifeEvent.objects.create(title="Summer Vacation")
        Scrobble.objects.create(
            user=user,
            life_event=life_event,
            media_type=Scrobble.MediaType.LIFE_EVENT,
            timestamp=_dt(10),
            stop_timestamp=_dt(8),
        )
        result = compute_fasting(user, period="last_30")
        fasting_dates = [d["date"] for d in result["fasting_days"]]
        assert _dt(9).strftime("%Y-%m-%d") not in fasting_dates

    def test_vacation_exemption_disabled(self, user, food):
        user.profile.fasting_vacation_exempt = False
        user.profile.save()
        _make_food(user, food, _dt(10, 12, 0))
        _make_food(user, food, _dt(8, 12, 0))
        life_event = LifeEvent.objects.create(title="Summer Vacation")
        Scrobble.objects.create(
            user=user,
            life_event=life_event,
            media_type=Scrobble.MediaType.LIFE_EVENT,
            timestamp=_dt(10),
            stop_timestamp=_dt(8),
        )
        result = compute_fasting(user, period="last_30")
        fasting_dates = [d["date"] for d in result["fasting_days"]]
        assert _dt(9).strftime("%Y-%m-%d") in fasting_dates

    def test_custom_thresholds(self, user_with_custom_thresholds, food):
        u = user_with_custom_thresholds
        _make_food(u, food, _dt(3, 12, 0))
        _make_food(u, food, _dt(2, 1, 0))
        result = compute_fasting(u, period="last_30")
        assert result["periodic_threshold_hours"] == 12
        assert result["full_threshold_hours"] == 20
        assert result["periodic_fasting_count"] == 1

    def test_fasting_rate(self, user, food):
        _make_food(user, food, _dt(10, 12, 0))
        _make_food(user, food, _dt(8, 12, 0))
        result = compute_fasting(user, period="last_30")
        assert result["total_days"] == 30
        assert result["fasting_days_count"] == 1
        assert result["fasting_rate_pct"] > 0

    def test_streaks(self, user, food):
        _make_food(user, food, _dt(10, 12, 0))
        _make_food(user, food, _dt(7, 12, 0))
        _make_food(user, food, _dt(4, 12, 0))
        result = compute_fasting(user, period="last_30")
        fasting_dates = [d["date"] for d in result["fasting_days"]]
        assert _dt(9).strftime("%Y-%m-%d") in fasting_dates
        assert _dt(8).strftime("%Y-%m-%d") in fasting_dates
        assert _dt(6).strftime("%Y-%m-%d") in fasting_dates
        assert _dt(5).strftime("%Y-%m-%d") in fasting_dates

    def test_mixed_fasting_types(self, user, food, drink):
        _make_food(user, food, _dt(10, 12, 0))
        _make_food(user, food, _dt(9, 6, 0))
        _make_food(user, food, _dt(7, 12, 0))
        _make_food(user, food, _dt(4, 12, 0))
        _make_drink(user, drink, _dt(5, 15, 0))
        result = compute_fasting(user, period="last_30")
        types = {d["date"]: d["type"] for d in result["fasting_days"]}
        assert types.get(_dt(9).strftime("%Y-%m-%d")) == "periodic"
        assert types.get(_dt(8).strftime("%Y-%m-%d")) == "full"
        assert types.get(_dt(6).strftime("%Y-%m-%d")) == "full"
        assert types.get(_dt(5).strftime("%Y-%m-%d")) == "full"
        day5 = next(
            d
            for d in result["fasting_days"]
            if d["date"] == _dt(5).strftime("%Y-%m-%d")
        )
        assert day5["had_drink"] is True

    @time_machine.travel(_dt(0, 12, 0))
    def test_yesterday_fasting(self, user, food):
        _make_food(user, food, _dt(2, 20, 0))
        _make_food(user, food, _dt(0, 10, 0))
        result = compute_fasting(user, period="last_30")
        assert result["fasting_days_count"] >= 1
        fasting_dates = [d["date"] for d in result["fasting_days"]]
        assert _dt(1).strftime("%Y-%m-%d") in fasting_dates

    def test_food_on_every_day_no_fasting(self, user, food):
        for i in range(30):
            _make_food(user, food, _dt(i, 12, 0))
        result = compute_fasting(user, period="last_30")
        assert result["fasting_days_count"] == 0
