import Testing
import Foundation
@testable import nahbar

// MARK: - CalendarEventStore Tests
//
// Testet das UserDefaults-basierte Mapping Moment-UUID → EKEvent-Identifier.
// CalendarManager selbst (EKEventStore, Berechtigungsfluss) erfordert einen
// echten Gerätezugang und ist daher nicht sinnvoll unit-testbar.

// Serialisiert, weil CalendarEventStore UserDefaults.standard shared state nutzt.
// Parallele Ausführung würde dazu führen, dass init()-Aufrufe verschiedener Tests
// den Key gegenseitig wegräumen.
@Suite("CalendarEventStore – CRUD", .serialized)
struct CalendarEventStoreCRUDTests {

    private let storeKey = "nahbar.momentCalendarEvents"

    /// Löscht den Mapping-Key vor jedem Test, um Seiteneffekte zu vermeiden.
    init() {
        UserDefaults.standard.removeObject(forKey: storeKey)
    }

    @Test("initial: kein Identifier für unbekannte UUID")
    func noIdentifierForUnknownUUID() {
        let unknownID = UUID()
        #expect(CalendarEventStore.identifier(for: unknownID) == nil)
    }

    @Test("save + identifier: gespeicherter Wert wird korrekt zurückgegeben")
    func saveAndRetrieve() {
        let momentID = UUID()
        let eventID = "EK-ABC-123"

        CalendarEventStore.save(momentID: momentID, eventIdentifier: eventID)

        #expect(CalendarEventStore.identifier(for: momentID) == eventID)
    }

    @Test("remove: nach dem Entfernen ist identifier nil")
    func removeDeletesEntry() {
        let momentID = UUID()
        CalendarEventStore.save(momentID: momentID, eventIdentifier: "to-delete")
        CalendarEventStore.remove(momentID: momentID)

        #expect(CalendarEventStore.identifier(for: momentID) == nil)
    }

    @Test("remove: nicht vorhandener Eintrag wirft keinen Fehler")
    func removeOfMissingEntryIsNoop() {
        let unknownID = UUID()
        // Darf keinen Crash oder Exception auslösen
        CalendarEventStore.remove(momentID: unknownID)
        #expect(CalendarEventStore.identifier(for: unknownID) == nil)
    }

    @Test("mehrere Einträge: jeder UUID erhält seinen eigenen Identifier")
    func multipleEntriesAreIndependent() {
        let ids = (0..<5).map { _ in UUID() }
        let eventIDs = ids.map { "event-\($0)" }

        for (id, eventID) in zip(ids, eventIDs) {
            CalendarEventStore.save(momentID: id, eventIdentifier: eventID)
        }

        for (id, eventID) in zip(ids, eventIDs) {
            #expect(CalendarEventStore.identifier(for: id) == eventID)
        }
    }

    @Test("remove betrifft nur den spezifischen Eintrag, andere bleiben erhalten")
    func removeDoesNotAffectOtherEntries() {
        let id1 = UUID()
        let id2 = UUID()
        CalendarEventStore.save(momentID: id1, eventIdentifier: "event-1")
        CalendarEventStore.save(momentID: id2, eventIdentifier: "event-2")

        CalendarEventStore.remove(momentID: id1)

        #expect(CalendarEventStore.identifier(for: id1) == nil)
        #expect(CalendarEventStore.identifier(for: id2) == "event-2")
    }

    @Test("überschreiben: zweites save ersetzt ersten Identifier")
    func overwriteUpdatesValue() {
        let momentID = UUID()
        CalendarEventStore.save(momentID: momentID, eventIdentifier: "old-event")
        CalendarEventStore.save(momentID: momentID, eventIdentifier: "new-event")

        #expect(CalendarEventStore.identifier(for: momentID) == "new-event")
    }

    @Test("identifier ist nach save deterministisch (mehrfache Abfrage gibt gleichen Wert)")
    func identifierIsDeterministic() {
        let momentID = UUID()
        let eventID = "stable-\(UUID().uuidString)"
        CalendarEventStore.save(momentID: momentID, eventIdentifier: eventID)

        let first  = CalendarEventStore.identifier(for: momentID)
        let second = CalendarEventStore.identifier(for: momentID)

        #expect(first == second)
        #expect(first == eventID)
    }

    @Test("UUID-String-Roundtrip: Schlüssel überlebt Serialisierung")
    func uuidStringKeyRoundTrip() {
        // Stellt sicher, dass UUID().uuidString als Dictionary-Key korrekt round-trippt
        let momentID = UUID()
        let eventID = "ek-\(UUID().uuidString)"

        CalendarEventStore.save(momentID: momentID, eventIdentifier: eventID)

        // Lade den Raw-Store und prüfe den Schlüssel direkt
        let raw = UserDefaults.standard.dictionary(forKey: storeKey) as? [String: String]
        #expect(raw?[momentID.uuidString] == eventID)
    }
}

// MARK: - Update-Logik

// CalendarManager.updateEvent greift auf EKEventStore (Gerätezugang) zu und ist
// daher nicht vollständig unit-testbar. Die folgenden Tests verifizieren die
// reinen Berechnungslogiken, die updateEvent intern verwendet.

@Suite("CalendarManager – Update-Logik")
struct CalendarManagerUpdateLogicTests {

    private let marker = "— via nahbar"

    // MARK: Notiz-Formatierung

    @Test("Notizen mit Text: marker wird durch zwei Zeilenumbrüche getrennt angehängt")
    func notesWithTextAppendMarker() {
        let text = "Schönes Treffen"
        let expected = "\(text)\n\n\(marker)"

        let result: String = {
            if !text.isEmpty {
                return "\(text)\n\n\(marker)"
            } else {
                return marker
            }
        }()

        #expect(result == expected)
    }

    @Test("Leerer Text: Notizen bestehen nur aus dem marker")
    func emptyTextProducesOnlyMarker() {
        let text = ""

        let result: String = {
            if !text.isEmpty {
                return "\(text)\n\n\(marker)"
            } else {
                return marker
            }
        }()

        #expect(result == marker)
    }

    @Test("Marker ist exakt '— via nahbar'")
    func markerFormat() {
        #expect(marker == "— via nahbar")
    }

    // MARK: Dauer-Berechnung

    @Test("Dauer bleibt erhalten wenn Startdatum verschoben wird")
    func durationPreservedOnShift() {
        let originalStart = Date(timeIntervalSinceReferenceDate: 0)
        let originalEnd   = Date(timeIntervalSinceReferenceDate: 3600) // 1 Stunde
        let newStart      = Date(timeIntervalSinceReferenceDate: 7200) // 2 Stunden später

        let duration = originalEnd.timeIntervalSince(originalStart)
        let newEnd   = newStart.addingTimeInterval(duration)

        #expect(duration == 3600)
        #expect(newEnd.timeIntervalSince(newStart) == 3600)
    }

    @Test("Startdatum-Verschiebung ändert Enddatum proportional")
    func endDateMovesWithStart() {
        let base     = Date(timeIntervalSinceReferenceDate: 1_000_000)
        let duration = TimeInterval(90 * 60) // 90 Minuten
        let shift    = TimeInterval(24 * 3600) // 1 Tag vorwärts

        let originalEnd = base.addingTimeInterval(duration)
        let newStart    = base.addingTimeInterval(shift)
        let newEnd      = newStart.addingTimeInterval(duration)

        #expect(newEnd.timeIntervalSince(newStart) == originalEnd.timeIntervalSince(base))
    }

    // MARK: isAllDay-Schutz

    @Test("isAllDay: Startdatum wird nicht verändert (Schutz-Flag)")
    func allDayEventSkipsDateShift() {
        let isAllDay   = true
        let origStart  = Date(timeIntervalSinceReferenceDate: 0)
        let newStart   = Date(timeIntervalSinceReferenceDate: 86400)

        // Simuliert den Guard in updateEvent
        var effectiveStart = origStart
        if !isAllDay {
            effectiveStart = newStart
        }

        #expect(effectiveStart == origStart)
    }

    @Test("nicht-Ganztages: Startdatum wird auf newStart gesetzt")
    func nonAllDayEventShiftsDate() {
        let isAllDay  = false
        let origStart = Date(timeIntervalSinceReferenceDate: 0)
        let newStart  = Date(timeIntervalSinceReferenceDate: 86400)

        var effectiveStart = origStart
        if !isAllDay {
            effectiveStart = newStart
        }

        #expect(effectiveStart == newStart)
    }
}

// MARK: - Alarm-Offset Semantik

@Suite("CalendarManager – Alarm-Offset-Semantik")
struct CalendarAlarmOffsetTests {

    @Test("Offset 0 wird als 'keine Erinnerung' interpretiert")
    func zeroOffsetMeansNoAlarm() {
        // Konvention: AddMomentView wandelt 0.0 → nil vor dem createEvent-Aufruf
        let rawOffset: Double = 0.0
        let alarmOffset: TimeInterval? = rawOffset == 0 ? nil : rawOffset
        #expect(alarmOffset == nil)
    }

    @Test("negativer Offset entspricht einem Zeitpunkt vor dem Event")
    func negativeOffsetIsBeforeEvent() {
        let oneHourBefore: TimeInterval = -3600
        #expect(oneHourBefore < 0)
    }

    @Test("Alarm-Offsets haben erwartete Werte")
    func alarmOffsetValues() {
        let offsets: [(String, Double)] = [
            ("5 Min",  -300),
            ("15 Min", -900),
            ("1 Std",  -3600),
            ("1 Tag",  -86400),
        ]
        for (label, offset) in offsets {
            #expect(offset < 0, "Offset '\(label)' muss negativ sein")
        }
    }

    @Test("1 Tag entspricht 86400 Sekunden")
    func oneDayIs86400Seconds() {
        #expect(-86400.0 == -(24 * 60 * 60))
    }

    @Test("1 Stunde entspricht 3600 Sekunden")
    func oneHourIs3600Seconds() {
        #expect(-3600.0 == -(60 * 60))
    }
}
