import Testing
import Foundation
@testable import nahbar

// MARK: - OceanDimension

@Suite("OceanDimension – Enum")
struct OceanDimensionTests {

    @Test("Genau 5 Dimensionen vorhanden")
    func allCasesCount() {
        #expect(OceanDimension.allCases.count == 5)
    }

    @Test("rawValues sind nicht leer")
    func rawValuesNotEmpty() {
        for dim in OceanDimension.allCases {
            #expect(!dim.rawValue.isEmpty, "\(dim) hat leeren rawValue")
        }
    }

    @Test("rawValue round-trip (init(rawValue:))")
    func rawValueRoundTrip() {
        for dim in OceanDimension.allCases {
            let recovered = OceanDimension(rawValue: dim.rawValue)
            #expect(recovered == dim, "\(dim.rawValue) kann nicht wiederhergestellt werden")
        }
    }

    @Test("shortLabel hat genau 1 Zeichen")
    func shortLabelLength() {
        for dim in OceanDimension.allCases {
            #expect(dim.shortLabel.count == 1, "\(dim) hat shortLabel '\(dim.shortLabel)' (nicht 1 Zeichen)")
        }
    }

    @Test("shortLabels sind eindeutig")
    func shortLabelsUnique() {
        let labels = OceanDimension.allCases.map { $0.shortLabel }
        #expect(Set(labels).count == labels.count, "Doppelte shortLabels: \(labels)")
    }

    @Test("Stabile rawValues – Regressionswächter")
    func stableRawValues() {
        #expect(OceanDimension.openness.rawValue          == "openness")
        #expect(OceanDimension.conscientiousness.rawValue == "conscientiousness")
        #expect(OceanDimension.extraversion.rawValue      == "extraversion")
        #expect(OceanDimension.agreeableness.rawValue     == "agreeableness")
        #expect(OceanDimension.neuroticism.rawValue       == "neuroticism")
    }

    @Test("axisLabel ist nicht leer")
    func axisLabelNotEmpty() {
        for dim in OceanDimension.allCases {
            #expect(!dim.axisLabel.isEmpty)
        }
    }

    @Test("icon ist nicht leer")
    func iconNotEmpty() {
        for dim in OceanDimension.allCases {
            #expect(!dim.icon.isEmpty)
        }
    }
}

// MARK: - TraitLevel

@Suite("TraitLevel – Schwellenwerte")
struct TraitLevelTests {

    @Test("Score 0 → niedrig")
    func score0IsLow()    { #expect(TraitLevel.from(score: 0) == .low) }

    @Test("Score 1 → mittel")
    func score1IsMedium() { #expect(TraitLevel.from(score: 1) == .medium) }

    @Test("Score 2 → hoch")
    func score2IsHigh()   { #expect(TraitLevel.from(score: 2) == .high) }

    @Test("Score >2 → hoch (overflow sicher)")
    func scoreOverflowIsHigh() { #expect(TraitLevel.from(score: 99) == .high) }

    @Test("rawValue round-trip")
    func rawValueRoundTrip() {
        for level in TraitLevel.allCases {
            #expect(TraitLevel(rawValue: level.rawValue) == level)
        }
    }

    @Test("Stabile rawValues – Regressionswächter")
    func stableRawValues() {
        #expect(TraitLevel.low.rawValue    == "low")
        #expect(TraitLevel.medium.rawValue == "medium")
        #expect(TraitLevel.high.rawValue   == "high")
    }
}

// MARK: - QuizQuestion

@Suite("QuizQuestion – Statische Fragen")
struct QuizQuestionTests {

    @Test("Genau 10 Fragen")
    func totalCount() {
        #expect(QuizQuestion.all.count == 10)
    }

    @Test("Genau 2 Fragen pro Dimension")
    func twoQuestionsPerDimension() {
        for dim in OceanDimension.allCases {
            let count = QuizQuestion.all.filter { $0.dimension == dim }.count
            #expect(count == 2, "Dimension \(dim.rawValue) hat \(count) statt 2 Fragen")
        }
    }

    @Test("Alle IDs sind eindeutig")
    func uniqueIDs() {
        let ids = QuizQuestion.all.map { $0.id }
        #expect(Set(ids).count == ids.count, "Doppelte Fragen-IDs: \(ids)")
    }

    @Test("Situationstexte sind nicht leer")
    func situationTextsNotEmpty() {
        for q in QuizQuestion.all {
            #expect(!q.situation.isEmpty, "Frage \(q.id) hat leeren Situationstext")
        }
    }

    @Test("Option-Texte sind nicht leer")
    func optionTextsNotEmpty() {
        for q in QuizQuestion.all {
            #expect(!q.optionA.isEmpty, "Frage \(q.id) hat leeren optionA-Text")
            #expect(!q.optionB.isEmpty, "Frage \(q.id) hat leeren optionB-Text")
        }
    }

    @Test("optionAScore ist 0 oder 1")
    func optionAScoreIsValid() {
        for q in QuizQuestion.all {
            #expect(q.optionAScore == 0 || q.optionAScore == 1,
                    "Frage \(q.id) hat ungültigen optionAScore: \(q.optionAScore)")
        }
    }

    @Test("Neurotizismus-Fragen haben optionAScore 0 (invertiert)")
    func neuroticismIsInverted() {
        let nQuestions = QuizQuestion.all.filter { $0.dimension == .neuroticism }
        for q in nQuestions {
            #expect(q.optionAScore == 0,
                    "Neurotizismus-Frage \(q.id) sollte optionAScore=0 haben (Option A = stabil)")
        }
    }

    @Test("Stabile Fragen-IDs – Regressionswächter")
    func stableQuestionIDs() {
        let ids = QuizQuestion.all.map { $0.id }
        #expect(ids == ["O1", "O2", "C1", "C2", "E1", "E2", "A1", "A2", "N1", "N2"])
    }
}

// MARK: - PersonalityEngine

@Suite("PersonalityEngine – Score-Berechnung")
struct PersonalityEngineTests {

    @Test("Alle Option-A → korrekter Score nach optionAScore")
    func allOptionAGivesCorrectScore() {
        let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
        let profile = PersonalityEngine.computeProfile(from: allA)

        for dim in OceanDimension.allCases {
            let expected = QuizQuestion.all
                .filter { $0.dimension == dim }
                .reduce(0) { $0 + $1.optionAScore }
            #expect(profile.scores[dim] == expected,
                    "\(dim.rawValue): erwartet \(expected), bekommen \(profile.scores[dim] ?? -1)")
        }
    }

    @Test("Alle Option-B → invertierter Score")
    func allOptionBGivesInvertedScore() {
        let allB = QuizQuestion.all.map { (questionID: $0.id, choseA: false) }
        let profile = PersonalityEngine.computeProfile(from: allB)

        for dim in OceanDimension.allCases {
            let expected = QuizQuestion.all
                .filter { $0.dimension == dim }
                .reduce(0) { $0 + (1 - $1.optionAScore) }
            #expect(profile.scores[dim] == expected,
                    "\(dim.rawValue): erwartet \(expected), bekommen \(profile.scores[dim] ?? -1)")
        }
    }

    @Test("Neurotizismus invertiertes Scoring: Option A = 0 Punkte, Option B = 1 Punkt")
    func neuroticismInvertedScoring() {
        let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
        let profileA = PersonalityEngine.computeProfile(from: allA)
        #expect(profileA.scores[.neuroticism] == 0,
                "N mit Option A: erwartet 0, bekommen \(profileA.scores[.neuroticism] ?? -1)")

        let allB = QuizQuestion.all.map { (questionID: $0.id, choseA: false) }
        let profileB = PersonalityEngine.computeProfile(from: allB)
        #expect(profileB.scores[.neuroticism] == 2,
                "N mit Option B: erwartet 2, bekommen \(profileB.scores[.neuroticism] ?? -1)")
    }

    @Test("Profil ist complete nach 10 Antworten")
    func profileIsCompleteAfterTenAnswers() {
        let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
        let profile = PersonalityEngine.computeProfile(from: allA)
        #expect(profile.isComplete)
        #expect(profile.completedAt != nil)
    }

    @Test("Scores liegen immer im Bereich 0…2")
    func scoresAlwaysInValidRange() {
        let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
        let profileA = PersonalityEngine.computeProfile(from: allA)
        let allB = QuizQuestion.all.map { (questionID: $0.id, choseA: false) }
        let profileB = PersonalityEngine.computeProfile(from: allB)

        for dim in OceanDimension.allCases {
            #expect((profileA.scores[dim] ?? -1) >= 0 && (profileA.scores[dim] ?? -1) <= 2)
            #expect((profileB.scores[dim] ?? -1) >= 0 && (profileB.scores[dim] ?? -1) <= 2)
        }
    }

    @Test("Fehlende Antworten (übersprungen) führen zu Score 0 für die Dimension")
    func skippedAnswersGiveZero() {
        let profile = PersonalityEngine.computeProfile(from: [])
        for dim in OceanDimension.allCases {
            #expect(profile.scores[dim] == 0, "\(dim.rawValue) sollte 0 sein bei leeren Antworten")
        }
    }

    @Test("completedAt liegt zwischen before und after dem Aufruf")
    func completedAtIsReasonablyNow() {
        let before = Date()
        let allA = QuizQuestion.all.map { (questionID: $0.id, choseA: true) }
        let profile = PersonalityEngine.computeProfile(from: allA)
        let after = Date()

        if let ts = profile.completedAt {
            #expect(ts >= before && ts <= after)
        } else {
            Issue.record("completedAt sollte gesetzt sein")
        }
    }
}

// MARK: - PersonalityProfile

@Suite("PersonalityProfile – Berechnung & Codable")
struct PersonalityProfileTests {

    private func makeProfile(scores: [OceanDimension: Int] = [:]) -> PersonalityProfile {
        var full = Dictionary(uniqueKeysWithValues: OceanDimension.allCases.map { ($0, 1) })
        for (k, v) in scores { full[k] = v }
        return PersonalityProfile(scores: full, completedAt: Date())
    }

    @Test("level für Score 0 → niedrig")
    func levelLowForScoreZero() {
        let p = makeProfile(scores: [.openness: 0])
        #expect(p.level(for: .openness) == .low)
    }

    @Test("level für Score 1 → mittel")
    func levelMediumForScoreOne() {
        let p = makeProfile(scores: [.openness: 1])
        #expect(p.level(for: .openness) == .medium)
    }

    @Test("level für Score 2 → hoch")
    func levelHighForScoreTwo() {
        let p = makeProfile(scores: [.openness: 2])
        #expect(p.level(for: .openness) == .high)
    }

    @Test("normalized: Score 0 → 0.0")
    func normalizedMinIsZero() {
        let p = makeProfile(scores: [.openness: 0])
        #expect(p.normalized(for: .openness) == 0.0)
    }

    @Test("normalized: Score 1 → 0.5")
    func normalizedMidIsFifty() {
        let p = makeProfile(scores: [.openness: 1])
        #expect(p.normalized(for: .openness) == 0.5)
    }

    @Test("normalized: Score 2 → 1.0")
    func normalizedMaxIsOne() {
        let p = makeProfile(scores: [.openness: 2])
        #expect(p.normalized(for: .openness) == 1.0)
    }

    @Test("summaryText ist nicht leer für alle Kombinationen")
    func summaryTextNeverEmpty() {
        for scores in [[TraitLevel.low, .low, .low, .low, .low],
                        [.high, .high, .high, .high, .high],
                        [.medium, .medium, .medium, .medium, .medium]] {
            let profile = PersonalityProfile(
                scores: Dictionary(uniqueKeysWithValues:
                    OceanDimension.allCases.enumerated().map { i, dim in
                        (dim, scores[i] == .low ? 0 : scores[i] == .medium ? 1 : 2)
                    }),
                completedAt: Date()
            )
            #expect(!profile.summaryText.isEmpty)
        }
    }

    @Test("isComplete ist false wenn completedAt nil")
    func isCompleteIsFalseWhenNilDate() {
        let p = PersonalityProfile(scores: [:], completedAt: nil)
        #expect(!p.isComplete)
    }

    @Test("isComplete ist true wenn completedAt gesetzt")
    func isCompleteIsTrueWhenDateSet() {
        let p = makeProfile()
        #expect(p.isComplete)
    }

    @Test("Codable round-trip via JSONEncoder/Decoder")
    func codableRoundTrip() throws {
        let original = makeProfile(scores: [.openness: 2, .extraversion: 0, .neuroticism: 1])
        let data = try JSONEncoder().encode(original)
        let decoded = try JSONDecoder().decode(PersonalityProfile.self, from: data)
        #expect(decoded == original)
    }

    @Test("PersonalityProfile benötigt keinen Netzwerkaufruf")
    func doesNotRequireNetwork() {
        // Rein synchron – kein await, kein async
        let p = makeProfile()
        let summary = p.summaryText
        #expect(!summary.isEmpty)
    }
}

// MARK: - PersonalityEngine – Behavior Logic

@Suite("PersonalityEngine – Verhaltenslogik")
struct PersonalityEngineBehaviorTests {

    private func profile(e: TraitLevel = .medium, n: TraitLevel = .medium,
                         c: TraitLevel = .medium, a: TraitLevel = .medium,
                         o: TraitLevel = .medium) -> PersonalityProfile {
        func score(_ l: TraitLevel) -> Int { l == .low ? 0 : l == .medium ? 1 : 2 }
        return PersonalityProfile(scores: [
            .extraversion: score(e), .neuroticism: score(n),
            .conscientiousness: score(c), .agreeableness: score(a), .openness: score(o)
        ], completedAt: Date())
    }

    @Test("Hohe Extraversion → kürzeres Nudge-Intervall als niedrige")
    func highExtraversionGivesShorterInterval() {
        let highE = PersonalityEngine.suggestedNudgeInterval(for: profile(e: .high))
        let lowE  = PersonalityEngine.suggestedNudgeInterval(for: profile(e: .low))
        #expect(highE < lowE)
    }

    @Test("Hoher Neurotizismus-Score + niedrige Extraversion → 14 Tage")
    func lowExtraversionHighNeuroticismGives14Days() {
        // N1 und N2 haben optionAScore=0, Option B gibt Punkte
        // In unserem Modell: hoher Neurotizismus-Score = mehr N-Punkte
        let p = profile(e: .low, n: .high)
        #expect(PersonalityEngine.suggestedNudgeInterval(for: p) == 14)
    }

    @Test("Hohe Gewissenhaftigkeit → sofortiger Rating-Prompt")
    func highConscientiousnessGivesImmediatePrompt() {
        let p = profile(c: .high)
        if case .immediate = PersonalityEngine.ratingPromptTiming(for: p) {
            // korrekt
        } else {
            Issue.record("Hohe Gewissenhaftigkeit sollte immediate prompt liefern")
        }
    }

    @Test("Hoher Neurotizismus → verzögerter Rating-Prompt (7200s)")
    func highNeuroticismGivesDelayedPrompt() {
        let p = profile(n: .high, c: .low)
        if case .delayed(let secs, _) = PersonalityEngine.ratingPromptTiming(for: p) {
            #expect(secs == 7200)
        } else {
            Issue.record("Hoher Neurotizismus sollte delayed prompt liefern")
        }
    }

    @Test("Benachrichtigungstext mit hohem Neurotizismus ist wärmer")
    func highNeuroticismNotificationCopyIsWarmer() {
        let p = profile(n: .high)
        let copy = PersonalityEngine.notificationCopy(contactName: "Alex", profile: p)
        #expect(copy.contains("freut sich"))
    }

    @Test("Benachrichtigungstext ohne Profil liefert Fallback")
    func nilProfileGivesFallback() {
        let copy = PersonalityEngine.notificationCopy(contactName: "Alex", profile: nil)
        #expect(!copy.isEmpty)
    }

    @Test("RecommendedBadge nicht angezeigt wenn Quiz übersprungen (kein Profil)")
    func noBadgeWhenNoProfile() {
        let suggestions = PersonalityEngine.sortedSuggestions(
            contacts: [],
            profile: nil,
            lastMeetingDates: [:]
        )
        for s in suggestions {
            #expect(!s.isRecommended)
        }
    }

}

// MARK: - GenderSelectionScreen Skip-Logik

@Suite("PersonalityQuiz – Geschlechtsabfrage überspringen")
struct PersonalityQuizGenderSkipTests {

    @Test("Wenn Gender gesetzt → GenderSelectionScreen wird übersprungen (geht zu questions)")
    func genderSetLeadsToQuestionsPhase() {
        // Spiegelt die nextPhaseAfterIntro()-Logik wider
        let gender = "Weiblich"
        let shouldSkip = !gender.isEmpty
        #expect(shouldSkip)
    }

    @Test("Wenn Gender leer → GenderSelectionScreen wird angezeigt")
    func emptyGenderShowsSelectionScreen() {
        let gender = ""
        let shouldShow = gender.isEmpty
        #expect(shouldShow)
    }

    @Test("Alle drei validen Werte überspringen den Screen")
    func allValidGenderValuesSkipScreen() {
        for gender in ["Männlich", "Weiblich", "Divers"] {
            #expect(!gender.isEmpty, "\(gender) sollte Screen überspringen")
        }
    }
}

// MARK: - Notification Copy Tests

@Suite("PersonalityEngine – Notification Copy")
struct NotificationCopyTests {

    private func profile(e: TraitLevel = .medium, n: TraitLevel = .medium,
                         c: TraitLevel = .medium) -> PersonalityProfile {
        func score(_ l: TraitLevel) -> Int { l == .low ? 0 : l == .medium ? 1 : 2 }
        return PersonalityProfile(scores: [
            .extraversion: score(e), .neuroticism: score(n),
            .conscientiousness: score(c), .agreeableness: 1, .openness: 1
        ], completedAt: Date())
    }

    // MARK: callWindowCopy

    @Test("callWindowCopy: nil-Profil liefert nicht-leeren Fallback")
    func callWindowCopyNilProfile() {
        let copy = PersonalityEngine.callWindowCopy(profile: nil)
        #expect(!copy.isEmpty)
    }

    @Test("callWindowCopy: hohe Extraversion → direkter Text")
    func callWindowCopyHighExtraversion() {
        let copy = PersonalityEngine.callWindowCopy(profile: profile(e: .high))
        #expect(copy.contains("freut sich"))
    }

    @Test("callWindowCopy: hoher Neurotizismus (nicht high E) → weicherer, ermutigender Text")
    func callWindowCopyHighNeuroticism() {
        let copy = PersonalityEngine.callWindowCopy(profile: profile(e: .low, n: .high))
        #expect(copy.contains("Magst du"))
    }

    @Test("callWindowCopy: Medium-Profil → Default-Text")
    func callWindowCopyDefault() {
        let copy = PersonalityEngine.callWindowCopy(profile: profile(e: .medium, n: .medium))
        #expect(!copy.isEmpty)
    }

    @Test("callWindowCopy: alle Varianten sind nicht leer")
    func callWindowCopyAllVariantsNonEmpty() {
        for e in TraitLevel.allCases {
            for n in TraitLevel.allCases {
                let copy = PersonalityEngine.callWindowCopy(profile: profile(e: e, n: n))
                #expect(!copy.isEmpty, "callWindowCopy leer für e=\(e), n=\(n)")
            }
        }
    }

    // MARK: aftermathCopy

    @Test("aftermathCopy: nil-Profil liefert nicht-leeren Fallback")
    func aftermathCopyNilProfile() {
        let copy = PersonalityEngine.aftermathCopy(profile: nil)
        #expect(!copy.isEmpty)
    }

    @Test("aftermathCopy: hoher Neurotizismus → weicher, einladender Text")
    func aftermathCopyHighNeuroticism() {
        let copy = PersonalityEngine.aftermathCopy(profile: profile(n: .high))
        #expect(copy.contains("Wenn du magst"))
    }

    @Test("aftermathCopy: niedriger Neurotizismus → direkter Text")
    func aftermathCopyLowNeuroticism() {
        let copy = PersonalityEngine.aftermathCopy(profile: profile(n: .low))
        #expect(copy.contains("Wie wirkt"))
    }

    @Test("aftermathCopy: alle Varianten sind nicht leer")
    func aftermathCopyAllVariantsNonEmpty() {
        for n in TraitLevel.allCases {
            let copy = PersonalityEngine.aftermathCopy(profile: profile(n: n))
            #expect(!copy.isEmpty, "aftermathCopy leer für n=\(n)")
        }
    }
}

// MARK: - OnboardingStep – Regressionswächter

@Suite("OnboardingStep – RawValues")
struct OnboardingStepQuizTests {

    @Test("RawValues sind aufsteigend 0–2")
    @MainActor func rawValuesSequential() {
        #expect(OnboardingStep.profile.rawValue  == 0)
        #expect(OnboardingStep.contacts.rawValue == 1)
        #expect(OnboardingStep.complete.rawValue == 2)
    }

    @Test("allCases enthält genau 3 Schritte")
    @MainActor func allCasesCountIsThree() {
        #expect(OnboardingStep.allCases.count == 3)
    }
}
