import Foundation
import Testing
@testable import Workouts

/// Locks `ReminderPlanner`'s pure trigger derivation — schedule snapshots in,
/// (identifier, DateComponents, repeats) plans out. `UNUserNotificationCenter`
/// itself is deliberately untested; the scheduler is a thin adapter over these plans.
struct ReminderPlannerTests {

    /// UTC Gregorian so nothing depends on the host machine's timezone.
    private let cal: Calendar = {
        var cal = Calendar(identifier: .gregorian)
        cal.timeZone = TimeZone(identifier: "UTC")!
        return cal
    }()

    /// Midnight UTC on an ISO `yyyy-MM-dd` date.
    private func day(_ iso: String) -> Date {
        let parts = iso.split(separator: "-").compactMap { Int($0) }
        var components = DateComponents()
        components.year = parts[0]; components.month = parts[1]; components.day = parts[2]
        return cal.date(from: components)!
    }

    private func source(
        id: String = "01SCHEDULE0000000000000001",
        routineName: String = "Push Day",
        recurrence: ScheduleRecurrence,
        weekdays: [Int]? = nil,
        date: Date? = nil,
        reminderMinutes: Int?
    ) -> ReminderSource {
        ReminderSource(id: id, routineName: routineName, recurrence: recurrence,
                       weekdays: weekdays, date: date, reminderMinutes: reminderMinutes)
    }

    // MARK: - Daily

    @Test func dailyProducesOneRepeatingHourMinuteTrigger() throws {
        let plans = ReminderPlanner.plans(
            schedules: [source(recurrence: .daily, reminderMinutes: 7 * 60 + 30)],
            calendar: cal, now: day("2026-07-11"))

        let plan = try #require(plans.first)
        #expect(plans.count == 1)
        #expect(plan.identifier == "reminder-01SCHEDULE0000000000000001")
        #expect(plan.routineName == "Push Day")
        #expect(plan.repeats)
        #expect(plan.dateComponents.hour == 7)
        #expect(plan.dateComponents.minute == 30)
        // Only hour+minute — no weekday/day, so it fires every day.
        #expect(plan.dateComponents.weekday == nil)
        #expect(plan.dateComponents.day == nil)
    }

    // MARK: - Fixed days

    @Test func fixedDaysProducesOneRepeatingTriggerPerWeekdaySorted() {
        let plans = ReminderPlanner.plans(
            schedules: [source(recurrence: .fixedDays, weekdays: [5, 2], reminderMinutes: 18 * 60)],
            calendar: cal, now: day("2026-07-11"))

        #expect(plans.count == 2)
        #expect(plans.map(\.identifier) == [
            "reminder-01SCHEDULE0000000000000001-2",
            "reminder-01SCHEDULE0000000000000001-5",
        ])
        #expect(plans.allSatisfy { $0.repeats })
        #expect(plans.map(\.dateComponents.weekday) == [2, 5])
        #expect(plans.allSatisfy { $0.dateComponents.hour == 18 && $0.dateComponents.minute == 0 })
    }

    @Test func fixedDaysIgnoresOutOfRangeWeekdaysAndEmptyListYieldsNothing() {
        let outOfRange = ReminderPlanner.plans(
            schedules: [source(recurrence: .fixedDays, weekdays: [0, 3, 9], reminderMinutes: 60)],
            calendar: cal, now: day("2026-07-11"))
        #expect(outOfRange.map(\.dateComponents.weekday) == [3])

        let empty = ReminderPlanner.plans(
            schedules: [source(recurrence: .fixedDays, weekdays: [], reminderMinutes: 60)],
            calendar: cal, now: day("2026-07-11"))
        #expect(empty.isEmpty)
    }

    // MARK: - Once

    @Test func onceFutureProducesOneNonRepeatingFullDateTrigger() throws {
        // Reminder at 06:15 on 2026-07-20; "now" is well before it.
        let plans = ReminderPlanner.plans(
            schedules: [source(recurrence: .once, date: day("2026-07-20"), reminderMinutes: 6 * 60 + 15)],
            calendar: cal, now: day("2026-07-11"))

        let plan = try #require(plans.first)
        #expect(plans.count == 1)
        #expect(!plan.repeats)
        #expect(plan.dateComponents.year == 2026)
        #expect(plan.dateComponents.month == 7)
        #expect(plan.dateComponents.day == 20)
        #expect(plan.dateComponents.hour == 6)
        #expect(plan.dateComponents.minute == 15)
    }

    @Test func oncePastMomentProducesNothing() {
        let reminderDay = day("2026-07-11")

        // The whole day is past.
        let dayPast = ReminderPlanner.plans(
            schedules: [source(recurrence: .once, date: reminderDay, reminderMinutes: 8 * 60)],
            calendar: cal, now: day("2026-07-12"))
        #expect(dayPast.isEmpty)

        // Same day, but the reminder minute (08:00) has already passed by noon.
        let minutePast = ReminderPlanner.plans(
            schedules: [source(recurrence: .once, date: reminderDay, reminderMinutes: 8 * 60)],
            calendar: cal, now: cal.date(byAdding: .hour, value: 12, to: reminderDay)!)
        #expect(minutePast.isEmpty)

        // Same day, reminder still ahead (20:00 vs noon) — scheduled.
        let stillAhead = ReminderPlanner.plans(
            schedules: [source(recurrence: .once, date: reminderDay, reminderMinutes: 20 * 60)],
            calendar: cal, now: cal.date(byAdding: .hour, value: 12, to: reminderDay)!)
        #expect(stillAhead.count == 1)
    }

    @Test func onceWithoutDateProducesNothing() {
        let plans = ReminderPlanner.plans(
            schedules: [source(recurrence: .once, date: nil, reminderMinutes: 8 * 60)],
            calendar: cal, now: day("2026-07-11"))
        #expect(plans.isEmpty)
    }

    // MARK: - No reminder

    @Test func nilReminderMinutesContributesNothingAcrossAllRecurrences() {
        let plans = ReminderPlanner.plans(
            schedules: [
                source(id: "A", recurrence: .daily, reminderMinutes: nil),
                source(id: "B", recurrence: .fixedDays, weekdays: [2, 4], reminderMinutes: nil),
                source(id: "C", recurrence: .once, date: day("2026-08-01"), reminderMinutes: nil),
            ],
            calendar: cal, now: day("2026-07-11"))
        #expect(plans.isEmpty)
    }

    // MARK: - Mixed batch

    @Test func mixedSchedulesFlattenInInputOrderWithPrefixOnEveryIdentifier() {
        let plans = ReminderPlanner.plans(
            schedules: [
                source(id: "A", recurrence: .daily, reminderMinutes: 6 * 60),
                source(id: "B", recurrence: .fixedDays, weekdays: [2, 6], reminderMinutes: 17 * 60),
                source(id: "C", recurrence: .daily, reminderMinutes: nil),
            ],
            calendar: cal, now: day("2026-07-11"))

        #expect(plans.map(\.identifier) == ["reminder-A", "reminder-B-2", "reminder-B-6"])
        #expect(plans.allSatisfy { $0.identifier.hasPrefix(ReminderPlanner.identifierPrefix) })
    }
}
