import Testing
import Foundation
@testable import nahbar

// MARK: - NudgeFrequency Tests

@Suite("NudgeFrequency")
struct NudgeFrequencyTests {

    @Test("never gibt nil zurück")
    func neverReturnsNil() {
        #expect(NudgeFrequency.never.days == nil)
    }

    @Test("weekly gibt 7 Tage zurück")
    func weeklyReturnsSeven() {
        #expect(NudgeFrequency.weekly.days == 7)
    }

    @Test("biweekly gibt 14 Tage zurück")
    func biweeklyReturnsFourteen() {
        #expect(NudgeFrequency.biweekly.days == 14)
    }

    @Test("monthly gibt 30 Tage zurück")
    func monthlyReturnsThirty() {
        #expect(NudgeFrequency.monthly.days == 30)
    }

    @Test("quarterly gibt 90 Tage zurück")
    func quarterlyReturnsNinety() {
        #expect(NudgeFrequency.quarterly.days == 90)
    }

    @Test("alle CaseIterable-Fälle haben korrekte Tage")
    func allCasesHaveValidDays() {
        for freq in NudgeFrequency.allCases {
            if freq == .never {
                #expect(freq.days == nil)
            } else {
                #expect(freq.days != nil)
                #expect(freq.days! > 0)
            }
        }
    }

    @Test("displayLabel ist nicht leer für alle Fälle")
    func displayLabelNotEmpty() {
        for freq in NudgeFrequency.allCases {
            #expect(!freq.displayLabel.isEmpty)
        }
    }

    @Test("biweekly displayLabel enthält '2 Wochen'")
    func biweeklyDisplayLabel() {
        #expect(NudgeFrequency.biweekly.displayLabel.contains("2 Wochen"))
    }
}

// MARK: - NudgeStatus Tests

@Suite("NudgeStatus")
struct NudgeStatusTests {

    private func makePerson(frequency: NudgeFrequency, lastContact: Date?) -> Person {
        let p = Person(name: "Test", tag: .friends)
        p.nudgeFrequency = frequency
        // lastMomentDate ist computed aus moments — wir simulieren via createdAt
        // Wir setzen createdAt auf den gewünschten Referenzzeitpunkt
        if let date = lastContact {
            p.createdAt = date
        }
        return p
    }

    @Test("never → .never Status")
    func neverFrequencyReturnsNever() {
        let p = makePerson(frequency: .never, lastContact: nil)
        #expect(p.nudgeStatus == .never)
    }

    @Test("kürzlicher Kontakt → .ok")
    func recentContactReturnsOk() {
        // Letzte Aktivität: gestern → weit unter 75 % des monatlichen Intervalls
        let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
        let p = makePerson(frequency: .monthly, lastContact: yesterday)
        #expect(p.nudgeStatus == .ok)
    }

    @Test("75–100 % des Intervalls → .soon")
    func approachingDeadlineReturnsSoon() {
        // 25 Tage her bei monatlichem Intervall (30 Tage) = 83 %
        let almostDue = Calendar.current.date(byAdding: .day, value: -25, to: Date())!
        let p = makePerson(frequency: .monthly, lastContact: almostDue)
        #expect(p.nudgeStatus == .soon)
    }

    @Test("über 100 % des Intervalls → .overdue")
    func overdueReturnsOverdue() {
        // 40 Tage her bei monatlichem Intervall (30 Tage)
        let tooLong = Calendar.current.date(byAdding: .day, value: -40, to: Date())!
        let p = makePerson(frequency: .monthly, lastContact: tooLong)
        #expect(p.nudgeStatus == .overdue)
    }

    @Test("wöchentlich + 8 Tage her → .overdue")
    func weeklyOverdue() {
        let eightDaysAgo = Calendar.current.date(byAdding: .day, value: -8, to: Date())!
        let p = makePerson(frequency: .weekly, lastContact: eightDaysAgo)
        #expect(p.nudgeStatus == .overdue)
    }

    @Test("nudgeStatus stimmt mit needsAttention überein wenn overdue")
    func nudgeStatusConsistentWithNeedsAttention() {
        let tooLong = Calendar.current.date(byAdding: .day, value: -40, to: Date())!
        let p = makePerson(frequency: .monthly, lastContact: tooLong)
        #expect(p.nudgeStatus == .overdue)
        #expect(p.needsAttention == true)
    }
}

// MARK: - PersonTag Tests

@Suite("PersonTag")
struct PersonTagTests {

    @Test("alle Tags haben ein nicht-leeres Icon")
    func allTagsHaveIcons() {
        for tag in PersonTag.allCases {
            #expect(!tag.icon.isEmpty)
        }
    }

    @Test("family hat house-Icon")
    func familyIconIsHouse() {
        #expect(PersonTag.family.icon == "house")
    }

    @Test("rawValue round-trip")
    func rawValueRoundTrip() {
        for tag in PersonTag.allCases {
            let parsed = PersonTag(rawValue: tag.rawValue)
            #expect(parsed == tag)
        }
    }
}

// MARK: - MomentType Tests

@Suite("MomentType")
struct MomentTypeTests {

    @Test("alle MomentTypes haben ein nicht-leeres Icon")
    func allTypesHaveIcons() {
        for type_ in MomentType.allCases {
            #expect(!type_.icon.isEmpty)
        }
    }

    @Test("rawValue round-trip")
    func rawValueRoundTrip() {
        for type_ in MomentType.allCases {
            let parsed = MomentType(rawValue: type_.rawValue)
            #expect(parsed == type_)
        }
    }

    @Test("displayName ist von rawValue entkoppelt für .conversation und .intention")
    func displayNameDecoupledFromRawValue() {
        // .conversation hat eigenen Anzeigenamen
        #expect(MomentType.conversation.displayName == "Gespräch/Chat")
        #expect(MomentType.conversation.rawValue == "Gespräch")   // Persistenz-Key bleibt
        // .meeting und .thought bleiben gleich
        #expect(MomentType.meeting.displayName == MomentType.meeting.rawValue)
        #expect(MomentType.thought.displayName == MomentType.thought.rawValue)
        // .intention (legacy) hat ebenfalls eigenen Anzeigenamen
        #expect(MomentType.intention.displayName == "Unternehmung")
        #expect(MomentType.intention.rawValue == "Vorhaben")       // Persistenz-Key bleibt
    }

    @Test("alle Types haben nicht-leeres displayName")
    func allTypesHaveNonEmptyDisplayName() {
        for type_ in MomentType.allCases {
            #expect(!type_.displayName.isEmpty)
        }
    }

    @Test("allCases enthält genau die 3 aktiven Typen – .intention ist ausgeblendet")
    func allCasesExcludesIntention() {
        let cases = MomentType.allCases
        #expect(cases.count == 3)
        #expect(cases.contains(.conversation))
        #expect(cases.contains(.meeting))
        #expect(cases.contains(.thought))
        #expect(!cases.contains(.intention))
    }

    @Test(".intention rawValue round-trip – Rückwärtskompatibilität")
    func intentionRawValueRoundTrip() {
        #expect(MomentType(rawValue: "Vorhaben") == .intention)
    }
}

// MARK: - MomentSource Tests

@Suite("MomentSource")
struct MomentSourceTests {

    @Test("alle Sources haben ein nicht-leeres Icon")
    func allSourcesHaveIcons() {
        for source in MomentSource.allCases {
            #expect(!source.icon.isEmpty)
        }
    }
}

// MARK: - Person Computed Properties Tests
// Person ist ein @Model – kann ohne Context instanziiert werden,
// solange nur non-Relationship-Properties getestet werden.

@Suite("Person – Computed Properties")
struct PersonComputedPropertyTests {

    @Test("initials aus Vor- und Nachname")
    func initialsFromFullName() {
        let p = Person(name: "Max Mustermann")
        #expect(p.initials == "MM")
    }

    @Test("initials aus einem Wort (2 Zeichen)")
    func initialsFromSingleWord() {
        let p = Person(name: "Max")
        #expect(p.initials == "MA")
    }

    @Test("initials aus drei Wörtern nimmt erste zwei")
    func initialsFromThreeWords() {
        let p = Person(name: "Max Karl Mustermann")
        #expect(p.initials == "MK")
    }

    @Test("initials sind uppercase")
    func initialsAreUppercase() {
        let p = Person(name: "anna bach")
        #expect(p.initials == p.initials.uppercased())
    }

    @Test("firstName aus vollem Namen")
    func firstNameFromFullName() {
        let p = Person(name: "Max Mustermann")
        #expect(p.firstName == "Max")
    }

    @Test("firstName bei einem Wort")
    func firstNameFromSingleWord() {
        let p = Person(name: "Max")
        #expect(p.firstName == "Max")
    }

    @Test("tag computed property round-trip")
    func tagRoundTrip() {
        let p = Person(name: "Test", tag: .family)
        #expect(p.tag == .family)
        p.tag = .work
        #expect(p.tag == .work)
        #expect(p.tagRaw == PersonTag.work.rawValue)
    }

    @Test("nudgeFrequency computed property round-trip")
    func nudgeFrequencyRoundTrip() {
        let p = Person(name: "Test", nudgeFrequency: .weekly)
        #expect(p.nudgeFrequency == .weekly)
        p.nudgeFrequency = .quarterly
        #expect(p.nudgeFrequency == .quarterly)
    }

    @Test("needsAttention mit .never ist immer false")
    func needsAttentionNeverIsAlwaysFalse() {
        let p = Person(name: "Test", nudgeFrequency: .never)
        #expect(!p.needsAttention)
    }

    @Test("needsAttention kurz nach Erstellung ist false")
    func needsAttentionJustCreatedIsFalse() {
        let p = Person(name: "Test", nudgeFrequency: .weekly)
        p.createdAt = Date()  // gerade erstellt
        #expect(!p.needsAttention)
    }

    @Test("needsAttention nach abgelaufener Nudge-Periode ist true")
    func needsAttentionAfterPeriodIsTrue() {
        let p = Person(name: "Test", nudgeFrequency: .weekly)
        // createdAt auf 10 Tage in der Vergangenheit setzen
        p.createdAt = Calendar.current.date(byAdding: .day, value: -10, to: Date())!
        #expect(p.needsAttention)
    }

    @Test("touch() aktualisiert updatedAt")
    func touchUpdatesTimestamp() throws {
        let p = Person(name: "Test")
        let before = p.updatedAt
        // Kurze Pause um unterschiedliche Timestamps zu garantieren
        Thread.sleep(forTimeInterval: 0.01)
        p.touch()
        #expect(p.updatedAt > before)
    }

    @Test("currentPhotoData bevorzugt photo gegenüber photoData")
    func currentPhotoDataFallback() {
        let p = Person(name: "Test")
        #expect(p.currentPhotoData == nil)  // Beide nil

        let legacyData = Data([0x01, 0x02])
        p.photoData = legacyData
        #expect(p.currentPhotoData == legacyData)  // Fallback auf legacy
    }

    @Test("hasBirthdayWithin(0) gibt immer false")
    func birthdayWithinZeroDays() {
        let p = Person(name: "Test", birthday: Date())
        #expect(!p.hasBirthdayWithin(days: 0))
    }

    @Test("hasBirthdayWithin erkennt heutigen Geburtstag")
    func birthdayWithinDetectsTodaysBirthday() {
        // Geburtstag auf heute setzen (Jahr egal, nur Monat+Tag zählt)
        let today = Date()
        let cal = Calendar.current
        var bdc = cal.dateComponents([.month, .day], from: today)
        bdc.year = 1990  // beliebiges vergangenes Jahr
        let birthday = cal.date(from: bdc)!
        let p = Person(name: "Test", birthday: birthday)
        #expect(p.hasBirthdayWithin(days: 1))
    }

    @Test("hasBirthdayWithin gibt false wenn Geburtstag außerhalb des Fensters")
    func birthdayOutsideWindowReturnsFalse() {
        let cal = Calendar.current
        // Geburtstag auf gestern setzen
        let yesterday = cal.date(byAdding: .day, value: -1, to: Date())!
        var bdc = cal.dateComponents([.month, .day], from: yesterday)
        bdc.year = 1990
        let birthday = cal.date(from: bdc)!
        let p = Person(name: "Test", birthday: birthday)
        // 1-Tage-Fenster ab heute: gestern liegt nicht darin
        #expect(!p.hasBirthdayWithin(days: 1))
    }
}

// MARK: - Moment Tests

@Suite("Moment – Computed Properties")
struct MomentComputedPropertyTests {

    @Test("type computed property round-trip")
    func typeRoundTrip() {
        let m = Moment(text: "Test", type: .meeting)
        #expect(m.type == .meeting)
        m.type = .thought
        #expect(m.type == .thought)
        #expect(m.typeRaw == MomentType.thought.rawValue)
    }

    @Test("source computed property round-trip")
    func sourceRoundTrip() {
        let m = Moment(text: "Test", source: .whatsapp)
        #expect(m.source == .whatsapp)
        m.source = .signal
        #expect(m.source == .signal)
    }

    @Test("source nil round-trip")
    func sourceNilRoundTrip() {
        let m = Moment(text: "Test", source: nil)
        #expect(m.source == nil)
        #expect(m.sourceRaw == nil)
    }

    @Test("isImportant startet als false")
    func isImportantDefaultsFalse() {
        let m = Moment(text: "Test")
        #expect(!m.isImportant)
    }

    // V5 – Meeting-Felder
    @Test("V5: statusRaw startet als nil")
    func statusRawDefaultsNil() {
        let m = Moment(text: "Test", type: .meeting)
        #expect(m.statusRaw == nil)
    }

    @Test("V5: aftermathNotificationScheduled startet als false")
    func aftermathNotificationScheduledDefaultsFalse() {
        let m = Moment(text: "Test")
        #expect(!m.aftermathNotificationScheduled)
    }

    @Test("V5: isCompleted startet als false")
    func isCompletedDefaultsFalse() {
        let m = Moment(text: "Test")
        #expect(!m.isCompleted)
    }

    @Test("V5: reminderDate startet als nil")
    func reminderDateDefaultsNil() {
        let m = Moment(text: "Test", type: .intention)
        #expect(m.reminderDate == nil)
    }

    @Test("V5: isMeeting true nur für .meeting")
    func isMeetingOnlyForMeetingType() {
        let meeting = Moment(text: "Test", type: .meeting)
        let other = Moment(text: "Test", type: .conversation)
        #expect(meeting.isMeeting)
        #expect(!other.isMeeting)
    }

    @Test("V5: isIntention true nur für .intention")
    func isIntentionOnlyForIntentionType() {
        let intention = Moment(text: "Test", type: .intention)
        let other = Moment(text: "Test", type: .thought)
        #expect(intention.isIntention)
        #expect(!other.isIntention)
    }

    @Test("V5: isOpen true wenn intention && nicht erledigt")
    func isOpenWhenNotCompleted() {
        let m = Moment(text: "Test", type: .intention)
        m.isCompleted = false
        #expect(m.isOpen)
        m.isCompleted = true
        #expect(!m.isOpen)
    }

    @Test("V5: meetingStatus round-trip via statusRaw")
    func meetingStatusRoundTrip() {
        let m = Moment(text: "Test", type: .meeting)
        m.meetingStatus = .awaitingAftermath
        #expect(m.meetingStatus == .awaitingAftermath)
        #expect(m.statusRaw == VisitStatus.awaitingAftermath.rawValue)
        m.meetingStatus = .completed
        #expect(m.meetingStatus == .completed)
        #expect(m.isMeetingComplete)
    }
}

// MARK: - Todo Tests

@Suite("Todo – Initialisierung und Computed Properties")
struct TodoTests {

    @Test("Todo startet mit isCompleted = false")
    func todoDefaultsNotCompleted() {
        let todo = Todo(title: "Test", dueDate: Date())
        #expect(!todo.isCompleted)
    }

    @Test("Todo startet mit completedAt = nil")
    func todoDefaultsCompletedAtNil() {
        let todo = Todo(title: "Test", dueDate: Date())
        #expect(todo.completedAt == nil)
    }

    @Test("Todo speichert Titel korrekt")
    func todoStoresTitleCorrectly() {
        let todo = Todo(title: "Anruf machen", dueDate: Date())
        #expect(todo.title == "Anruf machen")
    }

    @Test("Todo speichert dueDate korrekt")
    func todoStoresDueDateCorrectly() {
        let date = Calendar.current.date(byAdding: .day, value: 3, to: Date())!
        let todo = Todo(title: "Test", dueDate: date)
        #expect(todo.dueDate == date)
    }

    @Test("Person-Link ist nil wenn nicht angegeben")
    func todoPersonLinkIsNilByDefault() {
        let todo = Todo(title: "Test", dueDate: Date())
        #expect(todo.person == nil)
    }

    @Test("Person-Link kann beim Initialisieren gesetzt werden")
    func todoPersonLinkCanBeSet() {
        let person = Person(name: "Max Mustermann")
        let todo = Todo(title: "Anrufen", dueDate: Date(), person: person)
        #expect(todo.person?.name == "Max Mustermann")
    }

    @Test("Todo erhält bei Erstellung eine eindeutige UUID")
    func todoGetsUniqueID() {
        let todo1 = Todo(title: "Eins", dueDate: Date())
        let todo2 = Todo(title: "Zwei", dueDate: Date())
        #expect(todo1.id != todo2.id)
    }
}

// MARK: - LogEntry Tests

@Suite("LogEntry – Computed Properties")
struct LogEntryComputedPropertyTests {

    @Test("type computed property round-trip")
    func typeRoundTrip() {
        let entry = LogEntry(type: .call, title: "Test")
        #expect(entry.type == .call)
        entry.type = .nextStep
        #expect(entry.type == .nextStep)
        #expect(entry.typeRaw == LogEntryType.nextStep.rawValue)
    }

    @Test("alle LogEntryTypes haben ein nicht-leeres Icon und color")
    func allTypesHaveIconAndColor() {
        for type_ in LogEntryType.allCases {
            #expect(!type_.icon.isEmpty, "\(type_.rawValue) hat leeres icon")
            #expect(!type_.color.isEmpty, "\(type_.rawValue) hat leere color")
        }
    }

    @Test(".todoCompleted ist in allCases enthalten – Regressionswächter")
    func todoCompletedInAllCases() {
        #expect(LogEntryType.allCases.contains(.todoCompleted))
        #expect(LogEntryType.allCases.count == 4)
    }

    @Test("Stabile rawValues – Regressionswächter")
    func stableRawValues() {
        #expect(LogEntryType.nextStep.rawValue      == "Schritt abgeschlossen")
        #expect(LogEntryType.calendarEvent.rawValue == "Termin geplant")
        #expect(LogEntryType.call.rawValue          == "Anruf")
        #expect(LogEntryType.todoCompleted.rawValue == "Todo abgeschlossen")
    }
}
