package com.isidroid.job_sender

import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.constant.SendJobStatus
import com.isidroid.job.constant.sendJobStatusName
import com.isidroid.job.data.source.local.JobLocalSource
import com.isidroid.job.data.source.remote.JobNetworkSource
import com.isidroid.job.data.source.remote.api.ApiJob
import com.isidroid.job.model.PrintJob
import com.isidroid.job.repository.JobRepository
import com.isidroid.job_sender.data.source.local.SendJobLocalSource
import com.isidroid.job_sender.data.source.remote.SendJobNetworkSource
import com.isidroid.job_sender.domain.dto.JobSenderResult
import com.isidroid.job_sender.domain.model.PrintJobSender
import com.isidroid.job_sender.repository.JobSendRepositoryImpl
import com.isidroid.rendering.repository.RenderRepository
import com.isidroid.spot.repository.SpotRepository
import com.isidroid.test_utils.MockWebServerWrapper
import com.isidroid.test_utils.createApi
import com.isidroid.test_utils.isMethodPost
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class UploadJobsTest {
    private lateinit var api: ApiJob
    private lateinit var mockWebServer: MockWebServer
    private lateinit var renderRepository: RenderRepository
    private lateinit var printJobRepository: JobRepository
    private lateinit var repository: JobSendRepositoryImpl
    private lateinit var spotRepository: SpotRepository
    private lateinit var sendJobLocalSource: SendJobLocalSource
    private lateinit var sendJobNetworkSource: SendJobNetworkSource
    private lateinit var eventCollector: SendJobEventCollectorFlow
    private lateinit var mockContext: Context

    private val mockWrapper = MockWebServerWrapper { path, method, body ->
        when {
            path == "/api/job" && method.isMethodPost -> "create_job"
            else -> ""
        }
    }

    @Before
    fun setup() {
        mockContext = InstrumentationRegistry.getInstrumentation().context
        mockWebServer = mockWrapper.create()
        api = createApi(mockWebServer, ApiJob::class.java)
        renderRepository = mock(RenderRepository::class.java)
        spotRepository = mock(SpotRepository::class.java)
        printJobRepository = mock(JobRepository::class.java)
        sendJobLocalSource = mock(SendJobLocalSource::class.java)
        sendJobNetworkSource = mock(SendJobNetworkSource::class.java)
        eventCollector = SendJobEventCollectorFlow()

        repository = JobSendRepositoryImpl(mockContext, renderRepository, spotRepository, printJobRepository, sendJobLocalSource, sendJobNetworkSource, eventCollector)
    }

    @After
    fun shutdown() {
        mockWebServer.shutdown()
    }


    @Test
    fun testSuccessRenderAndUploadSinglePrintJob() = runBlocking {
        val delay = 100L
        val printJobSender = PrintJobSender(status = SendJobStatus.RENDERED, printJobId = "jobId")

        val items = listOf(printJobSender)
        val jobs = listOf(PrintJob(id = printJobSender.printJobId))

        Mockito.`when`(printJobRepository.readLocalList(ids = items.map { it.printJobId }.toList())).thenReturn(jobs)
        Mockito.`when`(printJobRepository.readLocalList(ids = emptyList())).thenReturn(emptyList())
        Mockito.`when`(sendJobNetworkSource.uploadPage(printJobSender.printJobId, "", "")).thenReturn(true)

        val events = mutableListOf<JobSenderResult>()
        val deferred = async { eventCollector.eventsFlow.toList(events) }

        delay(delay)
        val result = repository.readFilesAndSend(items)
        Assert.assertEquals(1, result.size)
        delay(delay)
        deferred.cancel()

        val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
        val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
        val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
        val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }

        Assert.assertEquals("invalid progress for first event", 0, actualEvent1Progress)
        Assert.assertEquals("invalid progress for last progress event", 1, actualEvent2Progress)
        Assert.assertEquals("incorrect count of rendered print jobs ready to be sent", 1, actualCountUploadStatus)
    }

    @Test
    fun testSuccessRenderAndUploadMultiplePrintJobs() = runBlocking {
        val delay = 100L
        val printJobSender = PrintJobSender(id = "1", status = SendJobStatus.RENDERED, printJobId = "jobId")

        val items = listOf(printJobSender, printJobSender.copy(id = "2"))
        val jobs = listOf(PrintJob(id = printJobSender.printJobId))

        Mockito.`when`(printJobRepository.readLocalList(ids = items.map { it.printJobId }.distinct().toList())).thenReturn(jobs)
        Mockito.`when`(printJobRepository.readLocalList(ids = emptyList())).thenReturn(emptyList())
        Mockito.`when`(sendJobNetworkSource.uploadPage(printJobSender.printJobId, "", "")).thenReturn(true)

        val events = mutableListOf<JobSenderResult>()
        val deferred = async { eventCollector.eventsFlow.toList(events) }

        delay(delay)
        val result = repository.readFilesAndSend(items)
        Assert.assertEquals(1, result.size)
        delay(delay)
        deferred.cancel()

        val actualEventTotalProgressCount = events.filterIsInstance<JobSenderResult.UploadProgress>().distinctBy { it.total }.let { if (it.size == 1) it[0].total else -1 }
        val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
        val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
        val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
        val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }

        Assert.assertEquals("invalid progress for first event", 0, actualEvent1Progress)
        Assert.assertEquals("invalid progress for last progress event ${actualEvent1Progress?.sendJobStatusName}", 1, actualEvent2Progress)
        Assert.assertEquals("incorrect count of rendered print jobs ready to be sent", 1, actualCountUploadStatus)
        Assert.assertEquals("incorrect count of total jobs", 2, actualEventTotalProgressCount)
    }

    @Test
    fun testFailRenderAndUploadSinglePrintJob() = runBlocking {
        val delay = 100L
        val printJobSender = PrintJobSender(status = SendJobStatus.RENDERED, printJobId = "jobId")

        val items = listOf(printJobSender)
        val jobs = listOf(PrintJob(id = printJobSender.printJobId))

        Mockito.`when`(printJobRepository.readLocalList(ids = items.map { it.printJobId }.toList())).thenReturn(jobs)
        Mockito.`when`(printJobRepository.readLocalList(ids = emptyList())).thenReturn(emptyList())
        Mockito.`when`(sendJobNetworkSource.uploadPage(printJobSender.printJobId, "", "")).thenReturn(false)

        val events = mutableListOf<JobSenderResult>()
        val deferred = async { eventCollector.eventsFlow.toList(events) }

        delay(delay)
        val result = repository.readFilesAndSend(items)
        Assert.assertEquals(0, result.size)
        delay(delay)
        deferred.cancel()

        val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
        val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
        val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
        val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }
        val actualEventJobErrorsCount = events.filterIsInstance<JobSenderResult.Error>().count()

        println("events=${events.mapIndexed { index, jobSenderResult -> "$index). ${jobSenderResult.javaClass.simpleName}" }}")

        Assert.assertEquals("invalid progress for first event", 0, actualEvent1Progress)
        Assert.assertEquals("invalid progress for last progress event", 1, actualEvent2Progress)
        Assert.assertEquals("incorrect count of rendered print jobs ready to be sent", 0, actualCountUploadStatus)
        Assert.assertEquals("error job events count", 1, actualEventJobErrorsCount)
    }

    @Test
    fun testSomeFails() = runBlocking {
        val delay = 100L
        val printJobSender = PrintJobSender(id = "1", status = SendJobStatus.RENDERED, printJobId = "jobId", accessToken = "1")

        val items = listOf(printJobSender, printJobSender.copy(id = "2", accessToken = "2"))
        val jobs = listOf(PrintJob(id = printJobSender.printJobId))

        Mockito.`when`(printJobRepository.readLocalList(ids = items.map { it.printJobId }.distinct().toList())).thenReturn(jobs)
        Mockito.`when`(printJobRepository.readLocalList(ids = emptyList())).thenReturn(emptyList())
        Mockito.`when`(sendJobNetworkSource.uploadPage(printJobSender.printJobId, "1", "")).thenReturn(true)
        Mockito.`when`(sendJobNetworkSource.uploadPage(printJobSender.printJobId, "2", "")).thenReturn(false)

        val events = mutableListOf<JobSenderResult>()
        val deferred = async { eventCollector.eventsFlow.toList(events) }

        delay(delay)
        val result = repository.readFilesAndSend(items)

        // we're expecting zero because there are some PrintJob with unsent jobs
        Assert.assertEquals(0, result.size)
        delay(delay)
        deferred.cancel()

        val actualEventTotalProgressCount = events.filterIsInstance<JobSenderResult.UploadProgress>().distinctBy { it.total }.let { if (it.size == 1) it[0].total else -1 }
        val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
        val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
        val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
        val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }

        Assert.assertEquals("invalid progress for first event", 0, actualEvent1Progress)
        Assert.assertEquals("invalid progress for last progress event", 1, actualEvent2Progress)
        Assert.assertEquals("incorrect count of rendered print jobs ready to be sent", 0, actualCountUploadStatus)
        Assert.assertEquals("incorrect count of total jobs", 2, actualEventTotalProgressCount)
    }

    @Test
    fun testOneJobSentButOtherDoesnt() = runBlocking {
        val delay = 100L
        val printJobSender = PrintJobSender(id = "1", status = SendJobStatus.RENDERED, printJobId = "jobId", accessToken = "1")
        val jobId2 = "jobId2"

        val items = listOf(
            printJobSender,
            printJobSender.copy(id = "2", accessToken = "2"),
            printJobSender.copy(id = "3", accessToken = "3", printJobId = jobId2)
        )

        val jobs = listOf(PrintJob(id = printJobSender.printJobId), PrintJob(id = jobId2))

        Mockito.`when`(printJobRepository.readLocalList(ids = items.map { it.printJobId }.distinct().toList())).thenReturn(jobs)
        Mockito.`when`(printJobRepository.readLocalList(ids = listOf(jobId2))).thenReturn(jobs)
        Mockito.`when`(printJobRepository.readLocalList(ids = listOf("jobId"))).thenReturn(jobs.filter { it.accessToken == "2" })
        Mockito.`when`(printJobRepository.readLocalList(ids = emptyList())).thenReturn(emptyList())
        Mockito.`when`(sendJobNetworkSource.uploadPage(printJobSender.printJobId, "1", "")).thenReturn(true)
        Mockito.`when`(sendJobNetworkSource.uploadPage(printJobSender.printJobId, "2", "")).thenReturn(false)
        Mockito.`when`(sendJobNetworkSource.uploadPage(jobId2, "3", "")).thenReturn(true)

        val events = mutableListOf<JobSenderResult>()
        val deferred = async { eventCollector.eventsFlow.toList(events) }

        delay(delay)
        val result = repository.readFilesAndSend(items)

        // we're expecting zero because there are some PrintJob with unsent jobs
        Assert.assertEquals(2, result.size)
        delay(delay)
        deferred.cancel()

        val actualEventTotalProgressCount = events.filterIsInstance<JobSenderResult.UploadProgress>().distinctBy { it.total }.let { if (it.size == 1) it[0].total else -1 }
        val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
        val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
        val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
        val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }

        Assert.assertEquals("invalid progress for first event", 0, actualEvent1Progress)
        Assert.assertEquals("invalid progress for last progress event", 1, actualEvent2Progress)
        Assert.assertEquals("incorrect count of rendered print jobs ready to be sent", 1, actualCountUploadStatus)
        Assert.assertEquals("incorrect count of total jobs", 3, actualEventTotalProgressCount)
    }
}