//
//  DataControllerCRUDTests.swift
//  ReflectTests
//
//  Tests for DataController create, read, update, and delete operations.
//

import XCTest
import SwiftData
@testable import Reflect

@MainActor
final class DataControllerCreateTests: XCTestCase {
    var sut: DataController!

    override func setUp() {
        super.setUp()
        let schema = Schema([MoodEntryModel.self])
        let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
        sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
    }

    override func tearDown() {
        sut = nil
        super.tearDown()
    }

    // MARK: - Phase 2: Create Tests

    func test_add_insertsEntry() {
        let date = makeDate(2024, 6, 15)
        sut.add(mood: .great, forDate: date, entryType: .listView)

        let entry = sut.getEntry(byDate: date)
        XCTAssertNotNil(entry, "Entry should exist after add")
        XCTAssertEqual(entry?.mood, .great)
        XCTAssertEqual(entry?.entryType, EntryType.listView.rawValue)
    }

    func test_add_replacesExistingForSameDate() {
        let date = makeDate(2024, 6, 15)
        sut.add(mood: .great, forDate: date, entryType: .listView)
        sut.add(mood: .bad, forDate: date, entryType: .widget)

        let allEntries = sut.getAllEntries(byDate: date)
        XCTAssertEqual(allEntries.count, 1, "Should have exactly 1 entry after 2 adds for same date")
        XCTAssertEqual(allEntries.first?.mood, .bad, "Should keep the latest mood")
    }

    func test_add_setsCorrectWeekday() {
        // June 15, 2024 is a Saturday (weekday = 7 in Gregorian)
        let date = makeDate(2024, 6, 15)
        sut.add(mood: .average, forDate: date, entryType: .listView)

        let entry = sut.getEntry(byDate: date)
        let expectedWeekday = Calendar.current.component(.weekday, from: date)
        XCTAssertEqual(entry?.weekDay, expectedWeekday)
    }

    func test_add_allMoodValues() {
        let moods: [Mood] = [.horrible, .bad, .average, .good, .great]
        for (i, mood) in moods.enumerated() {
            let date = makeDate(2024, 6, 10 + i)
            sut.add(mood: mood, forDate: date, entryType: .listView)

            let entry = sut.getEntry(byDate: date)
            XCTAssertEqual(entry?.mood, mood, "Mood \(mood) should persist and read back correctly")
        }
    }

    func test_addBatch_insertsMultiple() {
        let entries: [(mood: Mood, date: Date, entryType: EntryType)] = [
            (.great, makeDate(2024, 6, 10), .listView),
            (.good, makeDate(2024, 6, 11), .widget),
            (.average, makeDate(2024, 6, 12), .watch),
        ]
        sut.addBatch(entries: entries)

        for (mood, date, _) in entries {
            let entry = sut.getEntry(byDate: date)
            XCTAssertNotNil(entry, "Entry for \(date) should exist")
            XCTAssertEqual(entry?.mood, mood)
        }
    }

    func test_addBatch_replacesExisting() {
        let date = makeDate(2024, 6, 10)
        sut.add(mood: .horrible, forDate: date, entryType: .listView)

        let batchEntries: [(mood: Mood, date: Date, entryType: EntryType)] = [
            (.great, date, .widget),
        ]
        sut.addBatch(entries: batchEntries)

        let allEntries = sut.getAllEntries(byDate: date)
        XCTAssertEqual(allEntries.count, 1)
        XCTAssertEqual(allEntries.first?.mood, .great)
    }
}

// MARK: - Phase 3: Read Tests

@MainActor
final class DataControllerReadTests: XCTestCase {
    var sut: DataController!

    override func setUp() {
        super.setUp()
        let schema = Schema([MoodEntryModel.self])
        let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
        sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
    }

    override func tearDown() {
        sut = nil
        super.tearDown()
    }

    func test_getEntry_emptyDB_returnsNil() {
        let entry = sut.getEntry(byDate: Date())
        XCTAssertNil(entry)
    }

    func test_getEntry_boundaryExclusion() {
        // Insert entry at exactly midnight of Jan 2 (start of Jan 2)
        // Use hour=0 to place it precisely at the boundary
        let jan2Midnight = makeDate(2024, 1, 2, hour: 0)
        let entry = MoodEntryModel(forDate: jan2Midnight, mood: .great, entryType: .listView)
        sut.modelContext.insert(entry)
        sut.save()

        // Query for Jan 1 — should NOT find the Jan 2 entry
        // getEntry builds range [Jan 1 00:00, Jan 2 00:00] — with <= the midnight entry leaks in
        let jan1 = makeDate(2024, 1, 1)
        let result = sut.getEntry(byDate: jan1)
        XCTAssertNil(result, "Entry at midnight Jan 2 should NOT be returned when querying Jan 1")
    }

    func test_getData_filtersWeekdays() {
        // Add entries for a full week (Mon-Sun)
        // Jan 1, 2024 is a Monday (weekday=2)
        for i in 0..<7 {
            let date = Calendar.current.date(byAdding: .day, value: i, to: makeDate(2024, 1, 1))!
            sut.add(mood: .average, forDate: date, entryType: .listView)
        }

        // Filter to only weekdays 2 (Mon) and 6 (Fri)
        let startDate = makeDate(2024, 1, 1)
        let endDate = makeDate(2024, 1, 7)
        let results = sut.getData(startDate: startDate, endDate: endDate, includedDays: [2, 6])

        XCTAssertEqual(results.count, 2, "Should return only Monday and Friday")
    }

    func test_getData_emptyIncludedDays_returnsAll() {
        for i in 0..<7 {
            let date = Calendar.current.date(byAdding: .day, value: i, to: makeDate(2024, 1, 1))!
            sut.add(mood: .average, forDate: date, entryType: .listView)
        }

        let startDate = makeDate(2024, 1, 1)
        let endDate = makeDate(2024, 1, 7)
        let results = sut.getData(startDate: startDate, endDate: endDate, includedDays: [])

        XCTAssertEqual(results.count, 7, "Empty includedDays should return all 7 entries")
    }

    func test_splitIntoYearMonth_groupsCorrectly() {
        // Add entries in different months
        sut.add(mood: .great, forDate: makeDate(2024, 1, 15), entryType: .listView)
        sut.add(mood: .good, forDate: makeDate(2024, 1, 20), entryType: .listView)
        sut.add(mood: .bad, forDate: makeDate(2024, 3, 5), entryType: .listView)

        let grouped = sut.splitIntoYearMonth(includedDays: [])
        XCTAssertEqual(grouped[2024]?[1]?.count, 2, "Jan 2024 should have 2 entries")
        XCTAssertEqual(grouped[2024]?[3]?.count, 1, "Mar 2024 should have 1 entry")
        XCTAssertNil(grouped[2024]?[2], "Feb 2024 should have no entries")
    }

    func test_earliestEntry() {
        sut.add(mood: .great, forDate: makeDate(2024, 6, 15), entryType: .listView)
        sut.add(mood: .bad, forDate: makeDate(2024, 1, 1), entryType: .listView)
        sut.add(mood: .good, forDate: makeDate(2024, 3, 10), entryType: .listView)

        let earliest = sut.earliestEntry
        XCTAssertNotNil(earliest)
        XCTAssertTrue(Calendar.current.isDate(earliest!.forDate, inSameDayAs: makeDate(2024, 1, 1)))
    }

    func test_latestEntry() {
        sut.add(mood: .great, forDate: makeDate(2024, 6, 15), entryType: .listView)
        sut.add(mood: .bad, forDate: makeDate(2024, 1, 1), entryType: .listView)
        sut.add(mood: .good, forDate: makeDate(2024, 3, 10), entryType: .listView)

        let latest = sut.latestEntry
        XCTAssertNotNil(latest)
        XCTAssertTrue(Calendar.current.isDate(latest!.forDate, inSameDayAs: makeDate(2024, 6, 15)))
    }
}

// MARK: - Phase 4: Update Tests

@MainActor
final class DataControllerUpdateTests: XCTestCase {
    var sut: DataController!

    override func setUp() {
        super.setUp()
        let schema = Schema([MoodEntryModel.self])
        let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
        sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
    }

    override func tearDown() {
        sut = nil
        super.tearDown()
    }

    func test_update_changesMood() {
        let date = makeDate(2024, 6, 15)
        sut.add(mood: .bad, forDate: date, entryType: .listView)

        let result = sut.update(entryDate: date, withMood: .great)
        XCTAssertTrue(result)

        let entry = sut.getEntry(byDate: date)
        XCTAssertEqual(entry?.mood, .great)
    }

    func test_update_nonexistent_returnsFalse() {
        let result = sut.update(entryDate: makeDate(2024, 6, 15), withMood: .great)
        XCTAssertFalse(result)
    }

    func test_updateNotes_setsNotes() {
        let date = makeDate(2024, 6, 15)
        sut.add(mood: .good, forDate: date, entryType: .listView)

        let result = sut.updateNotes(forDate: date, notes: "Had a great day")
        XCTAssertTrue(result)

        let entry = sut.getEntry(byDate: date)
        XCTAssertEqual(entry?.notes, "Had a great day")
    }

    func test_updateNotes_nilClearsNotes() {
        let date = makeDate(2024, 6, 15)
        sut.add(mood: .good, forDate: date, entryType: .listView)
        sut.updateNotes(forDate: date, notes: "Some notes")

        let result = sut.updateNotes(forDate: date, notes: nil)
        XCTAssertTrue(result)

        let entry = sut.getEntry(byDate: date)
        XCTAssertNil(entry?.notes)
    }

    func test_updatePhoto_setsPhotoID() {
        let date = makeDate(2024, 6, 15)
        sut.add(mood: .good, forDate: date, entryType: .listView)

        let photoID = UUID()
        let result = sut.updatePhoto(forDate: date, photoID: photoID)
        XCTAssertTrue(result)

        let entry = sut.getEntry(byDate: date)
        XCTAssertEqual(entry?.photoID, photoID)
    }

    func test_update_refreshesTimestamp() {
        let date = makeDate(2024, 6, 15)
        sut.add(mood: .bad, forDate: date, entryType: .listView)

        let originalTimestamp = sut.getEntry(byDate: date)!.timestamp

        // Small delay so timestamp would differ
        let result = sut.update(entryDate: date, withMood: .great)
        XCTAssertTrue(result)

        let updatedEntry = sut.getEntry(byDate: date)!
        XCTAssertGreaterThanOrEqual(updatedEntry.timestamp, originalTimestamp,
            "Timestamp should be updated on mood change so removeDuplicates picks the right entry")
    }
}

// MARK: - Phase 5: Delete Tests

@MainActor
final class DataControllerDeleteTests: XCTestCase {
    var sut: DataController!

    override func setUp() {
        super.setUp()
        let schema = Schema([MoodEntryModel.self])
        let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
        sut = DataController(container: try! ModelContainer(for: schema, configurations: [config]))
    }

    override func tearDown() {
        sut = nil
        super.tearDown()
    }

    func test_clearDB_removesAll() {
        sut.add(mood: .great, forDate: makeDate(2024, 6, 10), entryType: .listView)
        sut.add(mood: .bad, forDate: makeDate(2024, 6, 11), entryType: .listView)
        sut.add(mood: .good, forDate: makeDate(2024, 6, 12), entryType: .listView)

        sut.clearDB()

        let all = sut.getData(startDate: Date(timeIntervalSince1970: 0), endDate: Date(), includedDays: [])
        XCTAssertTrue(all.isEmpty, "clearDB should remove all entries")
    }

    func test_deleteAllEntries_removesOnlyTargetDate() {
        let date1 = makeDate(2024, 6, 10)
        let date2 = makeDate(2024, 6, 11)
        sut.add(mood: .great, forDate: date1, entryType: .listView)
        sut.add(mood: .bad, forDate: date2, entryType: .listView)

        sut.deleteAllEntries(forDate: date1)

        XCTAssertNil(sut.getEntry(byDate: date1), "Deleted date should be gone")
        XCTAssertNotNil(sut.getEntry(byDate: date2), "Other dates should be untouched")
    }

    func test_removeDuplicates_keepsBestEntry() {
        let date = makeDate(2024, 6, 15)

        // Manually insert 3 entries for the same date to simulate CloudKit conflict
        let entry1 = MoodEntryModel(forDate: date, mood: .missing, entryType: .filledInMissing)
        entry1.timestamp = makeDate(2024, 6, 15, hour: 8)
        sut.modelContext.insert(entry1)

        let entry2 = MoodEntryModel(forDate: date, mood: .great, entryType: .listView)
        entry2.timestamp = makeDate(2024, 6, 15, hour: 10)
        sut.modelContext.insert(entry2)

        let entry3 = MoodEntryModel(forDate: date, mood: .good, entryType: .listView)
        entry3.timestamp = makeDate(2024, 6, 15, hour: 9)
        sut.modelContext.insert(entry3)

        sut.save()

        let removed = sut.removeDuplicates()
        XCTAssertEqual(removed, 2, "Should remove 2 duplicates")

        let remaining = sut.getAllEntries(byDate: date)
        XCTAssertEqual(remaining.count, 1, "Should keep exactly 1 entry")
        // Should keep non-missing with most recent timestamp (entry2, .great at 10am)
        XCTAssertEqual(remaining.first?.mood, .great, "Should keep the best entry (non-missing, most recent)")
    }
}

// MARK: - Test Helpers

func makeDate(_ year: Int, _ month: Int, _ day: Int, hour: Int = 12) -> Date {
    var components = DateComponents()
    components.year = year
    components.month = month
    components.day = day
    components.hour = hour
    components.minute = 0
    components.second = 0
    return Calendar.current.date(from: components)!
}
