import Testing
import Foundation
@testable import SharedModels

/// Invariants that the shipped `course_data.json` must satisfy for the
/// Complete the Sentence quiz to work for every card in every course.
///
/// These tests read the repo's `course_data.json` from a fixed relative path.
/// They act as the pass/fail oracle for the content gap-fill work: they fail
/// before the gap-fill pass is complete and pass once every card has at least
/// three examples and at least one of them yields a resolvable blank.
@Suite("Content coverage — course_data.json")
struct ContentCoverageTests {

    // Repo-relative path from this test file to the bundled data file.
    // SharedModels/Tests/SharedModelsTests/ContentCoverageTests.swift
    //   → ../../../../Conjuga/course_data.json
    private static let courseDataPath: String = {
        let here = URL(fileURLWithPath: #filePath)
        return here
            .deletingLastPathComponent()   // SharedModelsTests
            .deletingLastPathComponent()   // Tests
            .deletingLastPathComponent()   // SharedModels
            .deletingLastPathComponent()   // Conjuga (repo package parent)
            .appendingPathComponent("Conjuga/course_data.json")
            .path
    }()

    struct CardRef {
        let courseName: String
        let weekNumber: Int
        let deckTitle: String
        let front: String
        let back: String
        let examples: [[String: String]]
    }

    /// Load every card in course_data.json.
    static func loadAllCards() throws -> [CardRef] {
        let url = URL(fileURLWithPath: courseDataPath)
        let data = try Data(contentsOf: url)
        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
              let courses = json["courses"] as? [[String: Any]] else {
            Issue.record("course_data.json is not in the expected shape")
            return []
        }

        var cards: [CardRef] = []
        for course in courses {
            let cname = course["course"] as? String ?? "<unknown>"
            let weeks = course["weeks"] as? [[String: Any]] ?? []
            for week in weeks {
                let wnum = week["week"] as? Int ?? -1
                let decks = week["decks"] as? [[String: Any]] ?? []
                for deck in decks {
                    let title = deck["title"] as? String ?? "<unknown>"
                    let rawCards = deck["cards"] as? [[String: Any]] ?? []
                    for raw in rawCards {
                        let front = raw["front"] as? String ?? ""
                        let back = raw["back"] as? String ?? ""
                        let examples = (raw["examples"] as? [[String: String]]) ?? []
                        cards.append(CardRef(
                            courseName: cname,
                            weekNumber: wnum,
                            deckTitle: title,
                            front: front,
                            back: back,
                            examples: examples
                        ))
                    }
                }
            }
        }
        return cards
    }

    private static func vocabCard(from ref: CardRef) -> VocabCard {
        var exES: [String] = []
        var exEN: [String] = []
        var exBlanks: [String] = []
        for ex in ref.examples {
            if let es = ex["es"] {
                exES.append(es)
                exEN.append(ex["en"] ?? "")
                exBlanks.append(ex["blank"] ?? "")
            }
        }
        return VocabCard(
            front: ref.front,
            back: ref.back,
            deckId: "\(ref.courseName)_w\(ref.weekNumber)_\(ref.deckTitle)",
            examplesES: exES,
            examplesEN: exEN,
            examplesBlanks: exBlanks
        )
    }

    @Test("course_data.json exists and parses")
    func fileExists() throws {
        let cards = try Self.loadAllCards()
        #expect(cards.count > 0, "Expected at least one card in course_data.json")
    }

    @Test("Every card has at least three example sentences")
    func everyCardHasThreeExamples() throws {
        let cards = try Self.loadAllCards()
        var failures: [String] = []
        for ref in cards {
            if ref.examples.count < 3 {
                failures.append("[\(ref.courseName) / w\(ref.weekNumber) / \(ref.deckTitle)] '\(ref.front)' has \(ref.examples.count) examples")
            }
        }
        if !failures.isEmpty {
            let head = Array(failures.prefix(10)).joined(separator: "\n")
            Issue.record("\(failures.count) cards have fewer than 3 examples. First 10:\n\(head)")
        }
        #expect(failures.isEmpty)
    }

    @Test("Every card yields a resolvable SentenceQuizEngine question")
    func everyCardHasBlankableSentence() throws {
        let cards = try Self.loadAllCards()
        var failures: [String] = []
        for ref in cards {
            let card = Self.vocabCard(from: ref)
            if !SentenceQuizEngine.hasValidSentence(for: card) {
                failures.append("[\(ref.courseName) / w\(ref.weekNumber) / \(ref.deckTitle)] '\(ref.front)'")
            }
        }
        if !failures.isEmpty {
            let head = Array(failures.prefix(15)).joined(separator: "\n")
            Issue.record("\(failures.count) cards have no resolvable sentence for Complete the Sentence. First 15:\n\(head)")
        }
        #expect(failures.isEmpty)
    }

    @Test("Every generated question has a non-empty blank word and display template")
    func questionIntegrity() throws {
        let cards = try Self.loadAllCards()
        var failures: [String] = []
        for ref in cards {
            let card = Self.vocabCard(from: ref)
            // Try to build a question from each resolvable index deterministically
            for idx in SentenceQuizEngine.resolvableIndices(for: card) {
                guard let q = SentenceQuizEngine.buildQuestion(for: card, exampleIndex: idx) else {
                    failures.append("[\(ref.courseName) / '\(ref.front)'] index \(idx) returned nil despite being resolvable")
                    continue
                }
                if q.blankWord.isEmpty {
                    failures.append("[\(ref.courseName) / '\(ref.front)'] index \(idx) has empty blankWord")
                }
                if !q.displayTemplate.contains(SentenceQuizEngine.blankMarker) {
                    failures.append("[\(ref.courseName) / '\(ref.front)'] index \(idx) display template missing blank marker")
                }
                if q.displayTemplate == q.sentenceES {
                    failures.append("[\(ref.courseName) / '\(ref.front)'] index \(idx) display template unchanged from sentence")
                }
            }
        }
        if !failures.isEmpty {
            let head = Array(failures.prefix(10)).joined(separator: "\n")
            Issue.record("\(failures.count) question integrity failures. First 10:\n\(head)")
        }
        #expect(failures.isEmpty)
    }
}
