package com.syncflow.domain.sync

import com.syncflow.data.db.entities.SyncFileStateEntity
import com.syncflow.domain.model.ConflictStrategy
import com.syncflow.domain.model.DeleteBehavior
import com.syncflow.domain.model.RemoteFile
import com.syncflow.domain.model.SyncDirection
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.Instant

/**
 * End-to-end decision lifecycle for the backup scenario:
 *
 *   "I back up my phone to the cloud, then I delete the file on the phone.
 *    It must stay in the cloud."
 *
 * These walk the exact multi-cycle state the SyncEngine produces:
 *   - a successful UPLOAD saves state with the local mtime known and remote metadata null
 *     (SyncEngine.buildState(..., remoteAfterTransfer = null)),
 *   - the next sync sees both sides present and unchanged, returns SKIP, and the SKIP branch
 *     reconciles the record by filling in the remote metadata,
 *   - then the local file is deleted and we assert what happens to the cloud copy.
 *
 * The decision is driven entirely by deleteBehavior, so each terminal case is asserted for
 * KEEP, MIRROR, and ARCHIVE.
 */
class UploadBackupLifecycleTest {

    private val T0 = 1_716_393_136_000L // exact second boundary

    private fun local(ms: Long = T0, size: Long = 100L) =
        LocalFileInfo("photo.jpg", size, ms)

    private fun remote(ms: Long = T0, etag: String? = "etag1", size: Long = 100L) =
        RemoteFile("backup/photo.jpg", "photo.jpg", false, size, Instant.ofEpochMilli(ms), etag, null)

    /** Mirrors SyncEngine.buildState right after a successful UPLOAD: remote metadata still null. */
    private fun stateAfterUpload(ms: Long = T0) = SyncFileStateEntity(
        syncPairId = 1L, relativePath = "photo.jpg",
        localModifiedAt = Instant.ofEpochMilli(ms), localSizeBytes = 100L, localHash = null,
        remoteModifiedAt = null, remoteSizeBytes = 0L, remoteEtag = null,
        lastSyncedAt = Instant.now(), syncedHash = null,
    )

    /** Mirrors the record after the next sync's SKIP reconciliation fills in remote metadata. */
    private fun stateReconciled(ms: Long = T0, etag: String? = "etag1") = SyncFileStateEntity(
        syncPairId = 1L, relativePath = "photo.jpg",
        localModifiedAt = Instant.ofEpochMilli(ms), localSizeBytes = 100L, localHash = null,
        remoteModifiedAt = Instant.ofEpochMilli(ms), remoteSizeBytes = 100L, remoteEtag = etag,
        lastSyncedAt = Instant.now(), syncedHash = null,
    )

    private fun decide(
        local: LocalFileInfo?,
        remote: RemoteFile?,
        known: SyncFileStateEntity?,
        delete: DeleteBehavior,
    ) = syncDecide(
        SyncDirection.UPLOAD_ONLY, ConflictStrategy.KEEP_NEWEST, delete,
        local, remote, known, hasPriorSyncState = known != null,
    )

    // ── Cycle 1: first backup uploads the file ───────────────────────────────

    @Test fun `cycle 1 - first backup uploads regardless of delete behavior`() {
        assertEquals(SyncDecision.UPLOAD, decide(local(), null, null, DeleteBehavior.KEEP))
        assertEquals(SyncDecision.UPLOAD, decide(local(), null, null, DeleteBehavior.MIRROR))
        assertEquals(SyncDecision.UPLOAD, decide(local(), null, null, DeleteBehavior.ARCHIVE))
    }

    // ── Cycle 2: file present on both sides, unchanged -> SKIP (no deletion) ──

    @Test fun `cycle 2 - unchanged file skips right after upload (remote metadata still null)`() {
        assertEquals(SyncDecision.SKIP, decide(local(), remote(), stateAfterUpload(), DeleteBehavior.KEEP))
        assertEquals(SyncDecision.SKIP, decide(local(), remote(), stateAfterUpload(), DeleteBehavior.MIRROR))
    }

    @Test fun `cycle 2 - unchanged file skips once state is reconciled`() {
        assertEquals(SyncDecision.SKIP, decide(local(), remote(), stateReconciled(), DeleteBehavior.KEEP))
        assertEquals(SyncDecision.SKIP, decide(local(), remote(), stateReconciled(), DeleteBehavior.MIRROR))
    }

    // ── Cycle 3: deleted on the phone — THE scenario ─────────────────────────

    @Test fun `KEEP - deleting on phone leaves the cloud copy (correct backup behavior)`() {
        // Reconciled steady state:
        assertEquals(SyncDecision.SKIP, decide(null, remote(), stateReconciled(), DeleteBehavior.KEEP))
        // And even if deletion happens before the reconcile pass ran:
        assertEquals(SyncDecision.SKIP, decide(null, remote(), stateAfterUpload(), DeleteBehavior.KEEP))
    }

    @Test fun `MIRROR - deleting on phone DELETES the cloud copy (wrong for a backup)`() {
        assertEquals(SyncDecision.DELETE_REMOTE, decide(null, remote(), stateReconciled(), DeleteBehavior.MIRROR))
        assertEquals(SyncDecision.DELETE_REMOTE, decide(null, remote(), stateAfterUpload(), DeleteBehavior.MIRROR))
    }

    @Test fun `ARCHIVE - deleting on phone moves the cloud copy to _Deleted (preserved)`() {
        // syncDecide returns DELETE_REMOTE; the engine's DELETE_REMOTE branch MOVEs the file to
        // <remote>/_Deleted/ instead of removing it when deleteBehavior == ARCHIVE.
        assertEquals(SyncDecision.DELETE_REMOTE, decide(null, remote(), stateReconciled(), DeleteBehavior.ARCHIVE))
    }

    // ── After deletion: a brand-new remote file is NOT pulled down (upload-only) ─

    @Test fun `KEEP - a new remote file never comes down to the phone (upload-only)`() {
        // Remote-only, no state record: in upload-only this must SKIP, not DOWNLOAD.
        assertEquals(SyncDecision.SKIP, decide(null, remote(), null, DeleteBehavior.KEEP))
    }

    // ── Re-adding / changing a file after a KEEP deletion still uploads ───────

    @Test fun `KEEP - modifying the file locally still uploads the change`() {
        val newer = local(T0 + 5_000)
        assertEquals(SyncDecision.UPLOAD, decide(newer, remote(), stateReconciled(), DeleteBehavior.KEEP))
    }
}
