//
//  NotificationDeliveryDAOTests.swift
//  DailyNotificationPluginTests
//
//  Created by Matthew Raymer on 2025-12-08
//  Copyright © 2025 TimeSafari. All rights reserved.
//

import XCTest
import CoreData
@testable import DailyNotificationPlugin

/**
 * Unit tests for NotificationDeliveryDAO
 * 
 * Tests CRUD operations, query helpers, relationships, and cascade delete
 */
class NotificationDeliveryDAOTests: XCTestCase {
    
    var persistenceController: PersistenceController!
    var context: NSManagedObjectContext!
    
    override func setUp() {
        super.setUp()
        
        // Create in-memory Core Data stack
        persistenceController = PersistenceController(inMemory: true)
        context = persistenceController.viewContext
        
        XCTAssertNotNil(context, "Context should be available")
    }
    
    override func tearDown() {
        context = nil
        persistenceController = nil
        super.tearDown()
    }
    
    // MARK: - Create/Insert Tests
    
    func testCreate_WithAllParameters() {
        // Given: All parameters
        let deliveryTimestamp = Date()
        let id = UUID().uuidString
        let notificationId = UUID().uuidString
        
        // When: Create entity
        let entity = NotificationDelivery.create(
            in: context,
            id: id,
            notificationId: notificationId,
            timesafariDid: "test-did",
            deliveryTimestamp: deliveryTimestamp,
            deliveryStatus: "delivered",
            deliveryMethod: "local",
            deliveryAttemptNumber: 1,
            deliveryDurationMs: 100,
            userInteractionType: "tap",
            userInteractionTimestamp: deliveryTimestamp,
            userInteractionDurationMs: 50,
            errorCode: nil,
            errorMessage: nil,
            deviceInfo: "{\"model\":\"iPhone\"}",
            networkInfo: "{\"type\":\"wifi\"}",
            batteryLevel: 80,
            dozeModeActive: false,
            exactAlarmPermission: true,
            notificationPermission: true,
            metadata: "{\"key\":\"value\"}"
        )
        
        // Then: Entity should be created with correct values
        XCTAssertNotNil(entity, "Entity should be created")
        XCTAssertEqual(entity.id, id)
        XCTAssertEqual(entity.notificationId, notificationId)
        XCTAssertEqual(entity.timesafariDid, "test-did")
        XCTAssertEqual(entity.deliveryTimestamp, deliveryTimestamp)
        XCTAssertEqual(entity.deliveryStatus, "delivered")
        XCTAssertEqual(entity.deliveryMethod, "local")
        XCTAssertEqual(entity.deliveryAttemptNumber, 1)
        XCTAssertEqual(entity.deliveryDurationMs, 100)
        XCTAssertEqual(entity.userInteractionType, "tap")
        XCTAssertEqual(entity.userInteractionTimestamp, deliveryTimestamp)
        XCTAssertEqual(entity.userInteractionDurationMs, 50)
        XCTAssertEqual(entity.batteryLevel, 80)
        XCTAssertEqual(entity.dozeModeActive, false)
        XCTAssertEqual(entity.exactAlarmPermission, true)
        XCTAssertEqual(entity.notificationPermission, true)
    }
    
    func testCreate_WithRelationship() {
        // Given: NotificationContent entity
        let notificationId = UUID().uuidString
        let notification = NotificationContent.create(
            in: context,
            id: notificationId,
            scheduledTime: Date()
        )
        try! context.save()
        
        // When: Create delivery with relationship
        let delivery = NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: notificationId,
            notificationContent: notification,
            deliveryTimestamp: Date()
        )
        try! context.save()
        
        // Then: Relationship should be set
        XCTAssertNotNil(delivery.notificationContent, "Relationship should be set")
        XCTAssertEqual(delivery.notificationContent?.id, notificationId)
        
        // Verify inverse relationship
        XCTAssertTrue(notification.deliveries?.contains(delivery) ?? false, 
                     "Inverse relationship should be set")
    }
    
    func testCreate_FromDictionary_WithEpochMillis() {
        // Given: Dictionary with epoch milliseconds
        let deliveryTimestampMillis: Int64 = 1609459200000
        let dict: [String: Any] = [
            "id": "test-id",
            "notificationId": "notif-id",
            "deliveryTimestamp": deliveryTimestampMillis,
            "deliveryStatus": "delivered",
            "deliveryAttemptNumber": 1,
            "batteryLevel": 80
        ]
        
        // When: Create from dictionary
        let entity = NotificationDelivery.create(in: context, from: dict)
        
        // Then: Entity should be created with converted dates
        XCTAssertNotNil(entity, "Entity should be created")
        XCTAssertEqual(entity.id, "test-id")
        XCTAssertEqual(entity.notificationId, "notif-id")
        XCTAssertEqual(entity.deliveryStatus, "delivered")
        XCTAssertEqual(entity.deliveryAttemptNumber, 1)
        XCTAssertEqual(entity.batteryLevel, 80)
        
        // Verify date conversion
        let expectedDate = DailyNotificationDataConversions.dateFromEpochMillis(deliveryTimestampMillis)
        XCTAssertEqual(entity.deliveryTimestamp, expectedDate)
    }
    
    // MARK: - Read/Query Tests
    
    func testFetch_ById_Found() {
        // Given: Entity in database
        let id = UUID().uuidString
        let entity = NotificationDelivery.create(
            in: context,
            id: id,
            notificationId: UUID().uuidString,
            deliveryTimestamp: Date()
        )
        try! context.save()
        
        // When: Fetch by id
        let fetched = NotificationDelivery.fetch(by: id, in: context)
        
        // Then: Should find entity
        XCTAssertNotNil(fetched, "Should find entity")
        XCTAssertEqual(fetched?.id, id)
    }
    
    func testQuery_ByNotificationId() {
        // Given: Deliveries for different notifications
        let notificationId1 = UUID().uuidString
        let notificationId2 = UUID().uuidString
        
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: notificationId1,
            deliveryTimestamp: Date()
        )
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: notificationId1,
            deliveryTimestamp: Date()
        )
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: notificationId2,
            deliveryTimestamp: Date()
        )
        try! context.save()
        
        // When: Query by notificationId
        let results = NotificationDelivery.query(by: notificationId1, in: context)
        
        // Then: Should find only matching deliveries
        XCTAssertEqual(results.count, 2, "Should find 2 deliveries")
        XCTAssertTrue(results.allSatisfy { $0.notificationId == notificationId1 })
    }
    
    func testQuery_DeliveryTimestampBetween() {
        // Given: Deliveries with different timestamps
        let startDate = Date()
        let midDate = startDate.addingTimeInterval(3600) // 1 hour later
        let endDate = startDate.addingTimeInterval(7200) // 2 hours later
        let outsideDate = startDate.addingTimeInterval(10800) // 3 hours later
        
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: UUID().uuidString,
            deliveryTimestamp: startDate
        )
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: UUID().uuidString,
            deliveryTimestamp: midDate
        )
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: UUID().uuidString,
            deliveryTimestamp: endDate
        )
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: UUID().uuidString,
            deliveryTimestamp: outsideDate
        )
        try! context.save()
        
        // When: Query by deliveryTimestamp range
        let results = NotificationDelivery.query(
            deliveryTimestampBetween: startDate,
            and: endDate,
            in: context
        )
        
        // Then: Should find deliveries in range
        XCTAssertEqual(results.count, 3, "Should find 3 deliveries in range")
        XCTAssertTrue(results.allSatisfy { 
            $0.deliveryTimestamp! >= startDate && $0.deliveryTimestamp! <= endDate 
        })
    }
    
    func testQuery_ByDeliveryStatus() {
        // Given: Deliveries with different statuses
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: UUID().uuidString,
            deliveryStatus: "delivered",
            deliveryTimestamp: Date()
        )
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: UUID().uuidString,
            deliveryStatus: "delivered",
            deliveryTimestamp: Date()
        )
        NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: UUID().uuidString,
            deliveryStatus: "failed",
            deliveryTimestamp: Date()
        )
        try! context.save()
        
        // When: Query by deliveryStatus
        let results = NotificationDelivery.query(by: "delivered", in: context)
        
        // Then: Should find only matching deliveries
        XCTAssertEqual(results.count, 2, "Should find 2 deliveries")
        XCTAssertTrue(results.allSatisfy { $0.deliveryStatus == "delivered" })
    }
    
    // MARK: - Relationship Tests
    
    func testRelationship_OneToMany() {
        // Given: NotificationContent with multiple deliveries
        let notificationId = UUID().uuidString
        let notification = NotificationContent.create(
            in: context,
            id: notificationId,
            scheduledTime: Date()
        )
        
        let delivery1 = NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: notificationId,
            notificationContent: notification,
            deliveryTimestamp: Date()
        )
        let delivery2 = NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: notificationId,
            notificationContent: notification,
            deliveryTimestamp: Date()
        )
        try! context.save()
        
        // Then: Notification should have multiple deliveries
        let deliveries = notification.deliveries as? Set<NotificationDelivery>
        XCTAssertNotNil(deliveries, "Deliveries should be available")
        XCTAssertEqual(deliveries?.count, 2, "Should have 2 deliveries")
        XCTAssertTrue(deliveries?.contains(delivery1) ?? false)
        XCTAssertTrue(deliveries?.contains(delivery2) ?? false)
    }
    
    // MARK: - Cascade Delete Tests
    
    func testCascadeDelete_WhenNotificationContentDeleted() {
        // Given: NotificationContent with deliveries
        let notificationId = UUID().uuidString
        let notification = NotificationContent.create(
            in: context,
            id: notificationId,
            scheduledTime: Date()
        )
        
        let delivery1 = NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: notificationId,
            notificationContent: notification,
            deliveryTimestamp: Date()
        )
        let delivery2 = NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: notificationId,
            notificationContent: notification,
            deliveryTimestamp: Date()
        )
        try! context.save()
        
        // Verify deliveries exist
        let deliveriesBefore = NotificationDelivery.query(by: notificationId, in: context)
        XCTAssertEqual(deliveriesBefore.count, 2, "Should have 2 deliveries")
        
        // When: Delete NotificationContent
        NotificationContent.delete(by: notificationId, in: context)
        
        // Then: Deliveries should be cascade deleted
        let deliveriesAfter = NotificationDelivery.query(by: notificationId, in: context)
        XCTAssertEqual(deliveriesAfter.count, 0, "Deliveries should be cascade deleted")
        
        // Verify deliveries are actually deleted
        XCTAssertNil(NotificationDelivery.fetch(by: delivery1.id!, in: context))
        XCTAssertNil(NotificationDelivery.fetch(by: delivery2.id!, in: context))
    }
    
    // MARK: - Update Tests
    
    func testUpdateDeliveryStatus() {
        // Given: Entity with initial status
        let entity = NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: UUID().uuidString,
            deliveryStatus: "pending",
            deliveryTimestamp: Date()
        )
        try! context.save()
        
        // When: Update delivery status
        entity.updateDeliveryStatus("delivered")
        try! context.save()
        
        // Then: Status should be updated
        XCTAssertEqual(entity.deliveryStatus, "delivered")
    }
    
    func testRecordUserInteraction() {
        // Given: Entity without interaction
        let entity = NotificationDelivery.create(
            in: context,
            id: UUID().uuidString,
            notificationId: UUID().uuidString,
            deliveryTimestamp: Date()
        )
        try! context.save()
        
        // When: Record user interaction
        let interactionTime = Date()
        entity.recordUserInteraction(
            type: "tap",
            timestamp: interactionTime,
            durationMs: 100
        )
        try! context.save()
        
        // Then: Interaction should be recorded
        XCTAssertEqual(entity.userInteractionType, "tap")
        XCTAssertEqual(entity.userInteractionTimestamp, interactionTime)
        XCTAssertEqual(entity.userInteractionDurationMs, 100)
    }
    
    // MARK: - Delete Tests
    
    func testDelete_ById_Found() {
        // Given: Entity in database
        let id = UUID().uuidString
        NotificationDelivery.create(
            in: context,
            id: id,
            notificationId: UUID().uuidString,
            deliveryTimestamp: Date()
        )
        try! context.save()
        
        // When: Delete by id
        let deleted = NotificationDelivery.delete(by: id, in: context)
        
        // Then: Should be deleted
        XCTAssertTrue(deleted, "Should delete entity")
        
        // Verify deleted
        let fetched = NotificationDelivery.fetch(by: id, in: context)
        XCTAssertNil(fetched, "Entity should be deleted")
    }
    
    func testDeleteAll_ForNotificationId() {
        // Given: Multiple deliveries for a notification
        let notificationId = UUID().uuidString
        for i in 1...3 {
            NotificationDelivery.create(
                in: context,
                id: UUID().uuidString,
                notificationId: notificationId,
                deliveryTimestamp: Date()
            )
        }
        try! context.save()
        
        // When: Delete all for notification
        let count = NotificationDelivery.deleteAll(for: notificationId, in: context)
        
        // Then: Should delete all
        XCTAssertEqual(count, 3, "Should delete 3 deliveries")
        
        // Verify all deleted
        let remaining = NotificationDelivery.query(by: notificationId, in: context)
        XCTAssertEqual(remaining.count, 0, "Should be empty")
    }
    
    func testDeleteAll() {
        // Given: Multiple deliveries
        for i in 1...5 {
            NotificationDelivery.create(
                in: context,
                id: UUID().uuidString,
                notificationId: UUID().uuidString,
                deliveryTimestamp: Date()
            )
        }
        try! context.save()
        
        // When: Delete all
        let count = NotificationDelivery.deleteAll(in: context)
        
        // Then: Should delete all
        XCTAssertEqual(count, 5, "Should delete 5 deliveries")
        
        // Verify all deleted
        let all = NotificationDelivery.fetchAll(in: context)
        XCTAssertEqual(all.count, 0, "Should be empty")
    }
}

