import Testing
@testable import bookstax
import Foundation

// MARK: - TagDTO

@Suite("TagDTO")
struct TagDTOTests {

    @Test("id is composed of name and value")
    func idFormat() {
        let tag = TagDTO(name: "status", value: "published", order: 0)
        #expect(tag.id == "status:published")
    }

    @Test("id with empty value still includes colon separator")
    func idEmptyValue() {
        let tag = TagDTO(name: "featured", value: "", order: 0)
        #expect(tag.id == "featured:")
    }

    @Test("Two tags with same name/value have equal ids")
    func duplicateIds() {
        let t1 = TagDTO(name: "env", value: "prod", order: 0)
        let t2 = TagDTO(name: "env", value: "prod", order: 1)
        #expect(t1.id == t2.id)
    }

    @Test("Tags decode from JSON correctly")
    func decodeFromJSON() throws {
        let json = """
        {"name":"topic","value":"swift","order":3}
        """.data(using: .utf8)!
        let tag = try JSONDecoder().decode(TagDTO.self, from: json)
        #expect(tag.name == "topic")
        #expect(tag.value == "swift")
        #expect(tag.order == 3)
    }
}

// MARK: - PageDTO

private let iso8601Formatter: ISO8601DateFormatter = {
    let f = ISO8601DateFormatter()
    f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    return f
}()

private func pageJSON(
    slug: String? = nil,
    priority: Int? = nil,
    draft: Bool? = nil,
    tags: String? = nil,
    markdown: String? = nil
) -> Data {
    var fields: [String] = [
        #""id":1,"book_id":10,"name":"Test Page""#,
        #""created_at":"2024-01-01T00:00:00.000Z","updated_at":"2024-01-01T00:00:00.000Z""#
    ]
    if let s = slug    { fields.append("\"slug\":\"\(s)\"") }
    if let p = priority { fields.append("\"priority\":\(p)") }
    if let d = draft    { fields.append("\"draft\":\(d)") }
    if let t = tags     { fields.append("\"tags\":[\(t)]") }
    if let m = markdown { fields.append("\"markdown\":\"\(m)\"") }
    return ("{\(fields.joined(separator: ","))}").data(using: .utf8)!
}

private func makeDecoder() -> JSONDecoder {
    // Mirrors the decoder used in BookStackAPI
    let decoder = JSONDecoder()
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    decoder.dateDecodingStrategy = .custom { decoder in
        let container = try decoder.singleValueContainer()
        let str = try container.decode(String.self)
        if let date = formatter.date(from: str) { return date }
        // Fallback without fractional seconds
        let fallback = ISO8601DateFormatter()
        if let date = fallback.date(from: str) { return date }
        throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date: \(str)")
    }
    return decoder
}

@Suite("PageDTO – JSON Decoding")
struct PageDTOTests {

    @Test("Minimal JSON (no optional fields) decodes with defaults")
    func minimalDecoding() throws {
        let data = pageJSON()
        let page = try makeDecoder().decode(PageDTO.self, from: data)
        #expect(page.id == 1)
        #expect(page.bookId == 10)
        #expect(page.name == "Test Page")
        #expect(page.slug == "")           // defaults to ""
        #expect(page.priority == 0)        // defaults to 0
        #expect(page.draftStatus == false) // defaults to false
        #expect(page.tags.isEmpty)         // defaults to []
        #expect(page.markdown == nil)
        #expect(page.html == nil)
        #expect(page.chapterId == nil)
    }

    @Test("slug is decoded when present")
    func slugDecoded() throws {
        let data = pageJSON(slug: "my-page")
        let page = try makeDecoder().decode(PageDTO.self, from: data)
        #expect(page.slug == "my-page")
    }

    @Test("priority is decoded when present")
    func priorityDecoded() throws {
        let data = pageJSON(priority: 5)
        let page = try makeDecoder().decode(PageDTO.self, from: data)
        #expect(page.priority == 5)
    }

    @Test("draft true decodes correctly")
    func draftDecoded() throws {
        let data = pageJSON(draft: true)
        let page = try makeDecoder().decode(PageDTO.self, from: data)
        #expect(page.draftStatus == true)
    }

    @Test("tags array is decoded when present")
    func tagsDecoded() throws {
        let tagJSON = #"{"name":"lang","value":"swift","order":0}"#
        let data = pageJSON(tags: tagJSON)
        let page = try makeDecoder().decode(PageDTO.self, from: data)
        #expect(page.tags.count == 1)
        #expect(page.tags.first?.name == "lang")
    }

    @Test("markdown is decoded when present")
    func markdownDecoded() throws {
        let data = pageJSON(markdown: "# Hello")
        let page = try makeDecoder().decode(PageDTO.self, from: data)
        #expect(page.markdown == "# Hello")
    }
}

// MARK: - SearchResultDTO

@Suite("SearchResultDTO – JSON Decoding")
struct SearchResultDTOTests {

    private func resultJSON(withTags: Bool = false) -> Data {
        let tags = withTags ? #","tags":[{"name":"topic","value":"swift","order":0}]"# : ""
        return """
        {"id":7,"name":"Swift Basics","slug":"swift-basics","type":"page",
         "url":"https://example.com/books/1/page/7","preview":"An intro to Swift"\(tags)}
        """.data(using: .utf8)!
    }

    @Test("Result with no tags field defaults to empty array")
    func defaultsEmptyTags() throws {
        let result = try JSONDecoder().decode(SearchResultDTO.self, from: resultJSON())
        #expect(result.tags.isEmpty)
    }

    @Test("Result with tags decodes them correctly")
    func decodesTagsWhenPresent() throws {
        let result = try JSONDecoder().decode(SearchResultDTO.self, from: resultJSON(withTags: true))
        #expect(result.tags.count == 1)
    }

    @Test("All fields decode correctly")
    func allFieldsDecode() throws {
        let result = try JSONDecoder().decode(SearchResultDTO.self, from: resultJSON())
        #expect(result.id == 7)
        #expect(result.name == "Swift Basics")
        #expect(result.slug == "swift-basics")
        #expect(result.type == .page)
        #expect(result.preview == "An intro to Swift")
    }
}

// MARK: - SearchResultDTO.ContentType

@Suite("SearchResultDTO.ContentType")
struct ContentTypeTests {

    @Test("All content types have non-empty systemImage")
    func systemImagesNonEmpty() {
        for type_ in SearchResultDTO.ContentType.allCases {
            #expect(!type_.systemImage.isEmpty)
        }
    }

    @Test("Page system image is doc.text")
    func pageSystemImage() {
        #expect(SearchResultDTO.ContentType.page.systemImage == "doc.text")
    }

    @Test("Book system image is book.closed")
    func bookSystemImage() {
        #expect(SearchResultDTO.ContentType.book.systemImage == "book.closed")
    }

    @Test("Chapter system image is list.bullet.rectangle")
    func chapterSystemImage() {
        #expect(SearchResultDTO.ContentType.chapter.systemImage == "list.bullet.rectangle")
    }

    @Test("Shelf system image is books.vertical")
    func shelfSystemImage() {
        #expect(SearchResultDTO.ContentType.shelf.systemImage == "books.vertical")
    }

    @Test("ContentType decodes from raw string value")
    func rawValueDecoding() throws {
        let data = #""page""#.data(using: .utf8)!
        let type_ = try JSONDecoder().decode(SearchResultDTO.ContentType.self, from: data)
        #expect(type_ == .page)
    }
}

// MARK: - PaginatedResponse

@Suite("PaginatedResponse – Decoding")
struct PaginatedResponseTests {

    @Test("Paginated shelf response decodes total and data")
    func paginatedDecoding() throws {
        let json = """
        {"data":[{"id":1,"name":"My Shelf","slug":"my-shelf","description":"",
                  "created_at":"2024-01-01T00:00:00.000Z","updated_at":"2024-01-01T00:00:00.000Z"}],
         "total":1}
        """.data(using: .utf8)!
        let decoded = try makeDecoder().decode(PaginatedResponse<ShelfDTO>.self, from: json)
        #expect(decoded.total == 1)
        #expect(decoded.data.count == 1)
        #expect(decoded.data.first?.name == "My Shelf")
    }
}
