package com.syncflow

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.syncflow.data.db.entities.SyncFileStateEntity
import com.syncflow.data.providers.nextcloud.NextcloudProvider
import com.syncflow.domain.model.CloudAccount
import com.syncflow.domain.model.ConflictStrategy
import com.syncflow.domain.model.DeleteBehavior
import com.syncflow.domain.model.ProviderType
import com.syncflow.domain.model.SyncDirection
import com.syncflow.domain.sync.LocalFileInfo
import com.syncflow.domain.sync.SyncDecision
import com.syncflow.domain.sync.syncDecide
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.time.Instant

/**
 * Real end-to-end test against a live Nextcloud, run ON the device. Exercises the actual
 * NextcloudProvider (WebDAV over real TLS, including the atomic temp+MOVE upload) and proves
 * the backup guarantee: with Upload-only + KEEP, "deleted on phone" leaves the cloud copy.
 *
 * Credentials are passed as instrumentation args (never committed):
 *   adb shell am instrument -w \
 *     -e ncUrl https://nextcloud.khodak.me -e ncUser syncflow-test -e ncPass <pw> \
 *     com.syncflow.test/androidx.test.runner.AndroidJUnitRunner
 */
@RunWith(AndroidJUnit4::class)
class NextcloudIntegrationTest {

    private val ctx = InstrumentationRegistry.getInstrumentation().targetContext
    private val args = InstrumentationRegistry.getArguments()
    private val url = args.getString("ncUrl")
    private val user = args.getString("ncUser")
    private val pass = args.getString("ncPass")

    private fun provider(): NextcloudProvider {
        val account = CloudAccount(
            id = 1L,
            displayName = "IT",
            email = user,                       // Nextcloud dav path uses this
            providerType = ProviderType.NEXTCLOUD,
            credentialJson = """{"username":"$user","password":"$pass"}""",
            serverUrl = url,
            port = null,
        )
        return NextcloudProvider(account)
    }

    @Test
    fun fullBackupRoundTrip() = runBlocking {
        assumeTrue("ncUrl/ncUser/ncPass instrumentation args required", url != null && user != null && pass != null)
        val p = provider()
        val dir = "SyncFlowITest_${System.currentTimeMillis()}"
        val remoteFile = "$dir/hello.txt"
        val content = "SyncFlow integration test — 0 to 100 — ${System.currentTimeMillis()}".toByteArray()

        try {
            // 1. Connect
            assertTrue("testConnection failed", p.testConnection().isSuccess)

            // 2. Create the backup folder
            assertTrue("createDirectory failed", p.createDirectory(dir).isSuccess)

            // 3. Upload (exercises atomic temp-file + MOVE)
            val uploaded = p.uploadFile(ByteArrayInputStream(content), remoteFile, content.size.toLong())
            assertTrue("upload failed: ${uploaded.exceptionOrNull()}", uploaded.isSuccess)

            // 4. List — the file is on the cloud with the right size
            val listed = p.listFiles(dir).getOrThrow()
            val entry = listed.firstOrNull { it.name == "hello.txt" }
            assertNotNull("uploaded file not found in listing", entry)
            assertEquals("remote size mismatch", content.size.toLong(), entry!!.sizeBytes)

            // 5. Download — bytes round-trip intact
            val out = ByteArrayOutputStream()
            assertTrue("download failed", p.downloadFile(remoteFile, out).isSuccess)
            assertEquals("downloaded content mismatch", String(content), out.toString("UTF-8"))

            // 6. THE backup guarantee. Phone copy deleted, state record exists, Upload-only + KEEP.
            val known = SyncFileStateEntity(
                syncPairId = 1L, relativePath = "hello.txt",
                localModifiedAt = Instant.now(), localSizeBytes = content.size.toLong(), localHash = null,
                remoteModifiedAt = entry.modifiedAt, remoteSizeBytes = entry.sizeBytes, remoteEtag = entry.etag,
                lastSyncedAt = Instant.now(), syncedHash = null,
            )
            val keepDecision = syncDecide(
                SyncDirection.UPLOAD_ONLY, ConflictStrategy.KEEP_NEWEST, DeleteBehavior.KEEP,
                local = null, remote = entry, known = known, hasPriorSyncState = true,
            )
            assertEquals("KEEP must not delete the cloud copy", SyncDecision.SKIP, keepDecision)

            // ...and the engine would do nothing, so the file is verifiably STILL on the cloud:
            val stillThere = p.listFiles(dir).getOrThrow().any { it.name == "hello.txt" }
            assertTrue("cloud copy must survive a local delete under KEEP", stillThere)

            // 7. Contrast: MIRROR would delete it — prove the real DELETE works (also cleanup).
            val mirrorDecision = syncDecide(
                SyncDirection.UPLOAD_ONLY, ConflictStrategy.KEEP_NEWEST, DeleteBehavior.MIRROR,
                local = null, remote = entry, known = known, hasPriorSyncState = true,
            )
            assertEquals(SyncDecision.DELETE_REMOTE, mirrorDecision)
            assertTrue("deleteFile failed", p.deleteFile(remoteFile).isSuccess)
            val goneAfterDelete = p.listFiles(dir).getOrThrow().none { it.name == "hello.txt" }
            assertTrue("file should be gone after explicit remote delete", goneAfterDelete)
        } finally {
            runCatching { p.deleteFile(dir) } // best-effort cleanup of the test folder
        }
    }

    @Test
    fun chunkedUpload_assemblesLargeFileByteExact() = runBlocking {
        assumeTrue("ncUrl/ncUser/ncPass required", url != null && user != null && pass != null)
        // Tiny chunk size exercises multi-chunk assembly without needing a multi-GB file.
        val account = CloudAccount(
            id = 1, displayName = "IT", email = user, providerType = ProviderType.NEXTCLOUD,
            credentialJson = """{"username":"$user","password":"$pass"}""", serverUrl = url, port = null,
        )
        val p = NextcloudProvider(account, chunkSize = 1L * 1024 * 1024) // 1 MB chunks
        val dir = "SyncFlowChunk_${System.currentTimeMillis()}"
        try {
            p.createDirectory(dir).getOrThrow()
            val payload = ByteArray(5 * 1024 * 1024 + 7).also { java.util.Random(7).nextBytes(it) } // ~5 MB -> 6 chunks
            val up = p.uploadFile(ByteArrayInputStream(payload), "$dir/big.bin", payload.size.toLong())
            assertTrue("chunked upload failed: ${up.exceptionOrNull()}", up.isSuccess)
            assertEquals(payload.size.toLong(), p.listFiles(dir).getOrThrow().first { it.name == "big.bin" }.sizeBytes)
            val out = ByteArrayOutputStream(); p.downloadFile("$dir/big.bin", out).getOrThrow()
            assertArrayEquals("chunk-assembled content must equal the original bytes", payload, out.toByteArray())
        } finally {
            runCatching { p.deleteFile(dir) }
        }
    }

    /**
     * Real-world large-file test: streams a multi-GB file FROM THE PHONE through the app's
     * chunked-upload path to the external URL, verifies the full size landed, then cleans up.
     * Opt-in (slow): pass -e bigFileMB=<size>, e.g. 1536 for 1.5 GB.
     */
    @Test
    fun realWorld_largeFileChunkedUpload() = runBlocking {
        assumeTrue("ncUrl/ncUser/ncPass required", url != null && user != null && pass != null)
        val mb = args.getString("bigFileMB")?.toIntOrNull() ?: 0
        assumeTrue("pass -e bigFileMB=<size> to run the big-file test", mb > 0)

        val account = CloudAccount(
            id = 1, displayName = "IT", email = user, providerType = ProviderType.NEXTCLOUD,
            credentialJson = """{"username":"$user","password":"$pass"}""", serverUrl = url, port = null,
        )
        val p = NextcloudProvider(account) // default 100 MB chunks -> chunked path for >100 MB
        val dir = "SyncFlowBig_${System.currentTimeMillis()}"
        val tmp = File(ctx.cacheDir, "bigfile_${System.currentTimeMillis()}.bin")
        try {
            val total = mb.toLong() * 1024 * 1024
            FileOutputStream(tmp).use { os ->
                val buf = ByteArray(8 * 1024 * 1024)
                var written = 0L
                while (written < total) {
                    val n = minOf(buf.size.toLong(), total - written).toInt()
                    os.write(buf, 0, n); written += n
                }
            }
            assertEquals(total, tmp.length())
            p.createDirectory(dir).getOrThrow()
            val up = p.uploadFile(FileInputStream(tmp), "$dir/big.bin", tmp.length())
            assertTrue("large chunked upload failed: ${up.exceptionOrNull()}", up.isSuccess)
            assertEquals("full file size must land on the server", total,
                p.listFiles(dir).getOrThrow().first { it.name == "big.bin" }.sizeBytes)
        } finally {
            tmp.delete()
            runCatching { p.deleteFile(dir) }
        }
    }
}
