import Testing
import Foundation
import SwiftData
@testable import Proat

/// Tests voor het seeden van de woordenlijst (pack uit
/// dictionary/proat-dictionary.json). Twee dingen kunnen stil misgaan:
/// woorden die dubbel in de store belanden, en oude vervangregels die naast een
/// nieuwe blijven staan en het resultaat weer terugdraaien.
@MainActor
struct DictionarySeedTests {

    /// Verse in-memory store; raakt de echte SwiftData-store niet aan.
    private func makeContext() throws -> ModelContext {
        let schema = Schema([VocabularyWord.self, WordReplacement.self])
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try ModelContainer(for: schema, configurations: [config])
        return ModelContext(container)
    }

    /// Eigen defaults-domein per test. Nooit `UserDefaults.standard`: deze tests
    /// draaien in de app als test-host, dus dan zetten ze de seed-vlaggen van de
    /// echte Proat om en seedt de app bij de volgende start opnieuw.
    private func makeDefaults(_ name: String) -> UserDefaults {
        let suite = "com.helsdingen.proat.tests.\(name)"
        UserDefaults.standard.removePersistentDomain(forName: suite)
        return UserDefaults(suiteName: suite)!
    }

    @Test func seedsVocabularyFromPack() throws {
        let defaults = makeDefaults("seedsVocabulary")
        let context = try makeContext()

        DictionaryService.seedDefaultVocabulary(context: context, defaults: defaults)

        let words = try context.fetch(FetchDescriptor<VocabularyWord>())
        #expect(words.count == DictionaryPack.vocabulary.count)
        #expect(words.contains { $0.word == "Granit" })
        #expect(words.contains { $0.word == "Vakwark.ai" })
        #expect(words.contains { $0.word == "voorbelasting" })
    }

    @Test func seedingIsIdempotentAndCaseInsensitive() throws {
        let defaults = makeDefaults("idempotent")
        let context = try makeContext()

        // Woord dat al bestaat, met andere casing: mag niet dubbel komen.
        context.insert(VocabularyWord(word: "granit"))
        try context.save()

        DictionaryService.seedDefaultVocabulary(context: context, defaults: defaults)
        // Tweede keer met een verse vlag: mag niets meer toevoegen.
        DictionaryService.seedDefaultVocabulary(context: context,
                                                defaults: makeDefaults("idempotent2"))

        let words = try context.fetch(FetchDescriptor<VocabularyWord>())
        #expect(words.count == DictionaryPack.vocabulary.count)
        let granitCount = words.filter { $0.word.lowercased() == "granit" }.count
        #expect(granitCount == 1)
    }

    @Test func seedRemovesRuleThatPackAlreadyCovers() throws {
        let defaults = makeDefaults("removesCovered")
        let context = try makeContext()

        // De oude v6-regel: zelfde doel, kleinere variantenset, andere casing.
        // Die draaide "Te Kloeze" weer terug naar "te Kloeze".
        context.insert(WordReplacement(
            originalText: "Tekloeze, te kloeze, Te Kloeze, te Kloeze, tekloeze",
            replacementText: "te Kloeze"))
        try context.save()

        DictionaryService.seedDefaultReplacements(context: context, defaults: defaults)

        let rules = try context.fetch(FetchDescriptor<WordReplacement>())
        #expect(!rules.contains { $0.replacementText == "te Kloeze" })
        #expect(rules.contains { $0.replacementText == "Te Kloeze" })
        #expect(rules.count == DictionaryPack.replacements.count)
    }

    @Test func seedKeepsUserRuleThatPackDoesNotCover() throws {
        let defaults = makeDefaults("keepsUserRule")
        let context = try makeContext()

        context.insert(WordReplacement(originalText: "eigen trigger",
                                       replacementText: "eigen uitkomst"))
        try context.save()

        DictionaryService.seedDefaultReplacements(context: context, defaults: defaults)

        let rules = try context.fetch(FetchDescriptor<WordReplacement>())
        #expect(rules.contains { $0.originalText == "eigen trigger" })
        #expect(rules.count == DictionaryPack.replacements.count + 1)
    }

    @Test func packHasOneLiteralRulePerTarget() {
        // Twee letterlijke regels naar hetzelfde doel vechten om de sorteer-orde
        // (langste origineel eerst), waardoor de kortste het laatste woord heeft.
        var seen = Set<String>()
        for rule in DictionaryPack.replacements where !rule.original.hasPrefix("/") {
            let target = rule.replacement.lowercased()
            #expect(!seen.contains(target), "twee letterlijke regels naar \(rule.replacement)")
            seen.insert(target)
        }
    }

    @Test func vocabularyFitsInBiasingBudget() {
        // Boven het budget kapt composePrompt stil af aan de staart.
        let cost = DictionaryPack.vocabulary.reduce(0) { $0 + $1.count + 2 }
        #expect(cost <= DictionaryPack.biasBudgetChars)
    }
}
