import Foundation
import SwiftData
import Testing
@testable import Notes

@MainActor
struct NoteMapperTests {
    /// Returns the container too — `mainContext` does not keep its container
    /// alive, and letting it deallocate mid-test crashes the process.
    private func makeStore() throws -> (container: ModelContainer, context: ModelContext) {
        let config = ModelConfiguration(isStoredInMemoryOnly: true, cloudKitDatabase: .none)
        let container = try ModelContainer(for: NoteEntity.self, configurations: config)
        return (container, container.mainContext)
    }

    private func makeNote(text: String = "Barista's name is Sam") -> Note {
        var context = CaptureContext.empty
        context.latitude = 37.8044
        context.longitude = -122.2712
        context.placeName = "Blue Bottle Coffee"
        context.timeZoneID = "America/Los_Angeles"
        return Note.create(text: text, tags: ["coffee"], context: context)
    }

    @Test func upsertInsertsThenUpdates() throws {
        let (container, context) = try makeStore()
        defer { withExtendedLifetime(container) {} }
        let note = makeNote()

        NoteMapper.upsert(note, relativePath: note.relativePath, into: context)
        let inserted = NoteMapper.fetch(id: note.meta.id.stringValue, in: context)
        #expect(inserted?.text == "Barista's name is Sam")
        #expect(inserted?.placeName == "Blue Bottle Coffee")

        var edited = note
        edited.payload.text = "Barista's name is Sam — likes cortados"
        edited.payload.tags = ["coffee", "people"]
        NoteMapper.upsert(edited, relativePath: note.relativePath, into: context)

        let all = try context.fetch(FetchDescriptor<NoteEntity>())
        #expect(all.count == 1)
        #expect(all.first?.text == "Barista's name is Sam — likes cortados")
        #expect(all.first?.tags == ["coffee", "people"])
    }

    @Test func upsertWritesProjectAndNoteRestoresIt() throws {
        let (container, context) = try makeStore()
        defer { withExtendedLifetime(container) {} }
        var note = makeNote()
        note.payload.project = "Kitchen Remodel"
        NoteMapper.upsert(note, relativePath: note.relativePath, into: context)

        let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context))
        #expect(entity.project == "Kitchen Remodel")

        let restored = try #require(NoteMapper.note(from: entity))
        #expect(restored.payload.project == "Kitchen Remodel")
        #expect(restored.payload == note.payload)
    }

    @Test func entityRoundTripsBackToNote() throws {
        let (container, context) = try makeStore()
        defer { withExtendedLifetime(container) {} }
        let note = makeNote()
        NoteMapper.upsert(note, relativePath: note.relativePath, into: context)

        let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context))
        let restored = try #require(NoteMapper.note(from: entity))
        #expect(restored.payload == note.payload)
        #expect(restored.meta.id == note.meta.id)
    }

    // MARK: - Audio columns

    @Test func pendingAudioColumnsRoundTrip() throws {
        let (container, context) = try makeStore()
        defer { withExtendedLifetime(container) {} }
        var ctx = CaptureContext.empty
        ctx.device = "Watch"
        let audio = Note.AudioInfo(
            duration: 30,
            transcriberDeviceID: "install-42",
            transcriptionStatus: .pending,
            transcriptionLocaleID: nil,
            transcribedAt: nil
        )
        let note = Note.create(text: "", source: .transcribed, context: ctx, audio: audio)
        NoteMapper.upsert(note, relativePath: note.relativePath, into: context)

        let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context))
        #expect(entity.hasAudio)
        #expect(entity.audioDuration == 30)
        #expect(entity.transcriberDeviceID == "install-42")
        #expect(entity.transcriptionStatusRaw == "pending")
        #expect(entity.source == .transcribed)

        // A pending audio note carries no `transcribedAt`, so it reconstructs
        // losslessly (that field is the only one the cache intentionally drops).
        let restored = try #require(NoteMapper.note(from: entity))
        #expect(restored.payload == note.payload)
        #expect(restored.payload.audio == audio)
    }

    @Test func doneTranscriptionCachesColumnsButNotTranscribedAt() throws {
        let (container, context) = try makeStore()
        defer { withExtendedLifetime(container) {} }
        let audio = Note.AudioInfo(
            duration: 5,
            transcriberDeviceID: "install-7",
            transcriptionStatus: .done,
            transcriptionLocaleID: "en_US",
            transcribedAt: Date()
        )
        let note = Note.create(text: "Hello there", source: .transcribed, context: .empty, audio: audio)
        NoteMapper.upsert(note, relativePath: note.relativePath, into: context)

        let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context))
        #expect(entity.transcriptionStatusRaw == "done")
        #expect(entity.transcriptionLocaleID == "en_US")
        // The file is authoritative for the timestamp — the cache doesn't store it.
        #expect(entity.audio?.transcribedAt == nil)
        #expect(entity.audio?.transcriptionStatus == .done)
    }

    @Test func textNoteHasNoAudioColumns() throws {
        let (container, context) = try makeStore()
        defer { withExtendedLifetime(container) {} }
        let note = makeNote()
        NoteMapper.upsert(note, relativePath: note.relativePath, into: context)

        let entity = try #require(NoteMapper.fetch(id: note.meta.id.stringValue, in: context))
        #expect(!entity.hasAudio)
        #expect(entity.audio == nil)
        #expect(entity.audioDuration == nil)
        #expect(entity.transcriptionStatusRaw == nil)
    }
}
