package com.tt.honeyDue.notifications

import android.app.AlarmManager
import android.app.Application
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import com.tt.honeyDue.MainActivity
import com.tt.honeyDue.models.TaskCompletionCreateRequest
import com.tt.honeyDue.models.TaskCompletionResponse
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.widget.WidgetUpdateManager
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockkObject
import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config

/**
 * Unit tests for the iOS-parity [NotificationActionReceiver] (P4 Stream O).
 *
 * Covers the action dispatch table: Complete, Snooze, Open, Accept, Decline,
 * plus defensive handling of missing extras.
 */
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.TIRAMISU])
class NotificationActionReceiverTest {

    private lateinit var context: Context
    private lateinit var app: Application
    private lateinit var notificationManager: NotificationManager

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
        app = context.applicationContext as Application
        notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.cancelAll()
        mockkObject(APILayer)
        mockkObject(WidgetUpdateManager)
        every { WidgetUpdateManager.forceRefresh(any()) } just runs
    }

    @After
    fun tearDown() {
        unmockkAll()
        notificationManager.cancelAll()
    }

    // Build a receiver whose async work runs synchronously on the test scheduler.
    private fun receiverFor(scope: CoroutineScope): NotificationActionReceiver =
        NotificationActionReceiver().apply { coroutineScopeOverride = scope }

    private fun successCompletion(taskId: Int) = TaskCompletionResponse(
        id = 1,
        taskId = taskId,
        completedBy = null,
        completedAt = "2026-04-16T00:00:00Z",
        notes = "Completed from notification",
        actualCost = null,
        rating = null,
        images = emptyList(),
        createdAt = "2026-04-16T00:00:00Z",
        updatedTask = null
    )

    private fun postDummyNotification(id: Int) {
        // Create channels so the notify() call below actually posts on O+.
        NotificationChannels.ensureChannels(context)
        val n = androidx.core.app.NotificationCompat.Builder(context, NotificationChannels.TASK_REMINDER)
            .setSmallIcon(com.tt.honeyDue.R.mipmap.ic_launcher)
            .setContentTitle("t")
            .setContentText("b")
            .build()
        notificationManager.notify(id, n)
        assertTrue(
            "precondition: dummy notification should be posted",
            notificationManager.activeNotifications.any { it.id == id }
        )
    }

    // ---------- 1. COMPLETE dispatches to APILayer + cancels notification ----------

    @Test
    fun complete_callsCreateTaskCompletion_and_cancelsNotification() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val scope = CoroutineScope(SupervisorJob() + dispatcher)

        coEvery { APILayer.createTaskCompletion(any()) } returns ApiResult.Success(successCompletion(42))

        val notifId = 9001
        postDummyNotification(notifId)

        val intent = Intent(context, NotificationActionReceiver::class.java).apply {
            action = NotificationActions.COMPLETE
            putExtra(NotificationActions.EXTRA_TASK_ID, 42L)
            putExtra(NotificationActions.EXTRA_NOTIFICATION_ID, notifId)
        }
        receiverFor(scope).onReceive(context, intent)
        advanceUntilIdle()

        coVerify(exactly = 1) {
            APILayer.createTaskCompletion(match<TaskCompletionCreateRequest> {
                it.taskId == 42 && it.notes == "Completed from notification"
            })
        }
        verify(exactly = 1) { WidgetUpdateManager.forceRefresh(any()) }
        assertFalse(
            "notification should be canceled on success",
            notificationManager.activeNotifications.any { it.id == notifId }
        )

        scope.cancel()
    }

    // ---------- 2. COMPLETE failure: notification survives for retry ----------

    @Test
    fun complete_apiFailure_keepsNotification_forRetry() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val scope = CoroutineScope(SupervisorJob() + dispatcher)

        coEvery { APILayer.createTaskCompletion(any()) } returns ApiResult.Error("nope", 500)

        val notifId = 9002
        postDummyNotification(notifId)

        val intent = Intent(context, NotificationActionReceiver::class.java).apply {
            action = NotificationActions.COMPLETE
            putExtra(NotificationActions.EXTRA_TASK_ID, 7L)
            putExtra(NotificationActions.EXTRA_NOTIFICATION_ID, notifId)
        }
        receiverFor(scope).onReceive(context, intent)
        advanceUntilIdle()

        coVerify(exactly = 1) { APILayer.createTaskCompletion(any()) }
        verify(exactly = 0) { WidgetUpdateManager.forceRefresh(any()) }
        assertTrue(
            "notification should remain posted so the user can retry",
            notificationManager.activeNotifications.any { it.id == notifId }
        )

        scope.cancel()
    }

    // ---------- 3. SNOOZE: schedules AlarmManager +30 min ----------

    @Test
    fun snooze_schedulesAlarm_thirtyMinutesOut() = runTest {
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)

        val notifId = 9003
        postDummyNotification(notifId)

        val beforeMs = System.currentTimeMillis()
        val intent = Intent(context, NotificationActionReceiver::class.java).apply {
            action = NotificationActions.SNOOZE
            putExtra(NotificationActions.EXTRA_TASK_ID, 55L)
            putExtra(NotificationActions.EXTRA_NOTIFICATION_ID, notifId)
            putExtra(NotificationActions.EXTRA_TITLE, "Title")
            putExtra(NotificationActions.EXTRA_BODY, "Body")
            putExtra(NotificationActions.EXTRA_TYPE, NotificationChannels.TASK_REMINDER)
        }
        receiverFor(scope).onReceive(context, intent)

        val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val scheduled = shadowOf(am).scheduledAlarms
        assertEquals("exactly one snooze alarm scheduled", 1, scheduled.size)

        val alarm = scheduled.first()
        val delta = alarm.triggerAtTime - beforeMs
        val expected = NotificationActions.SNOOZE_DELAY_MS
        // Allow ±2s jitter around the expected 30 minutes.
        assertTrue(
            "snooze alarm should fire ~30 min out (delta=$delta)",
            delta in (expected - 2_000)..(expected + 2_000)
        )
        assertFalse(
            "original notification should be cleared after snooze",
            notificationManager.activeNotifications.any { it.id == notifId }
        )

        scope.cancel()
    }

    // ---------- 4. OPEN: launches MainActivity with deep-link ----------

    @Test
    fun open_launchesMainActivity_withDeepLinkAndExtras() = runTest {
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)

        val notifId = 9004
        postDummyNotification(notifId)

        val intent = Intent(context, NotificationActionReceiver::class.java).apply {
            action = NotificationActions.OPEN
            putExtra(NotificationActions.EXTRA_TASK_ID, 77L)
            putExtra(NotificationActions.EXTRA_RESIDENCE_ID, 3L)
            putExtra(NotificationActions.EXTRA_DEEP_LINK, "honeydue://task/77")
            putExtra(NotificationActions.EXTRA_NOTIFICATION_ID, notifId)
        }
        receiverFor(scope).onReceive(context, intent)

        val started = shadowOf(app).nextStartedActivity
        assertNotNull("MainActivity should be launched", started)
        assertEquals(MainActivity::class.java.name, started.component?.className)
        assertEquals("honeydue", started.data?.scheme)
        assertEquals("77", started.data?.pathSegments?.last())
        assertEquals(77L, started.getLongExtra(FcmService.EXTRA_TASK_ID, -1))
        assertEquals(3L, started.getLongExtra(FcmService.EXTRA_RESIDENCE_ID, -1))
        assertFalse(
            "notification should be canceled after open",
            notificationManager.activeNotifications.any { it.id == notifId }
        )

        scope.cancel()
    }

    // ---------- 5. ACCEPT_INVITE: calls APILayer + clears notification ----------

    @Test
    fun acceptInvite_withResidenceId_cancelsNotification() = runTest {
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)

        coEvery { APILayer.acceptResidenceInvite(any()) } returns ApiResult.Success(Unit)

        val notifId = 9005
        postDummyNotification(notifId)

        val intent = Intent(context, NotificationActionReceiver::class.java).apply {
            action = NotificationActions.ACCEPT_INVITE
            putExtra(NotificationActions.EXTRA_RESIDENCE_ID, 101L)
            putExtra(NotificationActions.EXTRA_NOTIFICATION_ID, notifId)
        }
        receiverFor(scope).onReceive(context, intent)

        coVerify(exactly = 1) { APILayer.acceptResidenceInvite(101) }
        assertFalse(
            "invite notification should be cleared on accept",
            notificationManager.activeNotifications.any { it.id == notifId }
        )

        scope.cancel()
    }

    // ---------- 6. Missing extras: no crash, no-op ----------

    @Test
    fun complete_withoutTaskId_isNoOp() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val scope = CoroutineScope(SupervisorJob() + dispatcher)

        val notifId = 9006
        postDummyNotification(notifId)

        val intent = Intent(context, NotificationActionReceiver::class.java).apply {
            action = NotificationActions.COMPLETE
            putExtra(NotificationActions.EXTRA_NOTIFICATION_ID, notifId)
            // no task_id
        }
        receiverFor(scope).onReceive(context, intent)
        advanceUntilIdle()

        coVerify(exactly = 0) { APILayer.createTaskCompletion(any()) }
        assertTrue(
            "notification must survive a malformed COMPLETE",
            notificationManager.activeNotifications.any { it.id == notifId }
        )

        scope.cancel()
    }

    // ---------- 7. Unknown action: no-op ----------

    @Test
    fun unknownAction_isNoOp() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val scope = CoroutineScope(SupervisorJob() + dispatcher)

        val intent = Intent(context, NotificationActionReceiver::class.java).apply {
            action = "com.tt.honeyDue.action.NONSENSE"
            putExtra(NotificationActions.EXTRA_TASK_ID, 1L)
        }
        receiverFor(scope).onReceive(context, intent)
        advanceUntilIdle()

        coVerify(exactly = 0) { APILayer.createTaskCompletion(any()) }
        verify(exactly = 0) { WidgetUpdateManager.forceRefresh(any()) }
        // No started activity either.
        assertNull(shadowOf(app).nextStartedActivity)

        scope.cancel()
    }

    // ---------- 8. Null action: no crash ----------

    @Test
    fun nullAction_doesNotCrash() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val scope = CoroutineScope(SupervisorJob() + dispatcher)

        val intent = Intent() // action is null
        // Should not throw.
        receiverFor(scope).onReceive(context, intent)
        advanceUntilIdle()

        coVerify(exactly = 0) { APILayer.createTaskCompletion(any()) }

        scope.cancel()
    }

    // ---------- 9. Decline invite: clears notification ----------

    @Test
    fun declineInvite_withResidenceId_cancelsNotification() = runTest {
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)

        coEvery { APILayer.declineResidenceInvite(any()) } returns ApiResult.Success(Unit)

        val notifId = 9009
        postDummyNotification(notifId)

        val intent = Intent(context, NotificationActionReceiver::class.java).apply {
            action = NotificationActions.DECLINE_INVITE
            putExtra(NotificationActions.EXTRA_RESIDENCE_ID, 77L)
            putExtra(NotificationActions.EXTRA_NOTIFICATION_ID, notifId)
        }
        receiverFor(scope).onReceive(context, intent)

        coVerify(exactly = 1) { APILayer.declineResidenceInvite(77) }
        assertFalse(
            "invite notification should be cleared on decline",
            notificationManager.activeNotifications.any { it.id == notifId }
        )

        scope.cancel()
    }
}
