package com.syncflow.domain.sync

import com.syncflow.data.db.entities.SyncFileStateEntity
import com.syncflow.domain.model.*
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.Instant

class SyncDecideTest {

    private val MS  = 1_716_000_000_000L
    private val MS2 = MS + 5_000L

    private fun local(ms: Long = MS, size: Long = 100L) = LocalFileInfo("test.txt", size, ms)

    private fun remote(ms: Long = MS, etag: String? = "abc", size: Long = 100L) =
        RemoteFile(
            path = "path/test.txt", name = "test.txt", isDirectory = false,
            sizeBytes = size, modifiedAt = Instant.ofEpochMilli(ms),
            etag = etag, mimeType = null,
        )

    private fun state(localMs: Long? = MS, remoteMs: Long? = MS, etag: String? = "abc") =
        SyncFileStateEntity(
            syncPairId = 1L, relativePath = "test.txt",
            localModifiedAt  = localMs?.let  { Instant.ofEpochMilli(it) },
            localSizeBytes   = 100L, localHash = null,
            remoteModifiedAt = remoteMs?.let { Instant.ofEpochMilli(it) },
            remoteSizeBytes  = 100L, remoteEtag = etag,
            lastSyncedAt = Instant.now(), syncedHash = null,
        )

    private fun decide(
        local: LocalFileInfo?, remote: RemoteFile?, known: SyncFileStateEntity? = null,
        dir: SyncDirection = SyncDirection.TWO_WAY,
        conflict: ConflictStrategy = ConflictStrategy.KEEP_NEWEST,
        delete: DeleteBehavior = DeleteBehavior.MIRROR,
        hasPriorState: Boolean = known != null,
    ) = syncDecide(dir, conflict, delete, local, remote, known, hasPriorState)

    // ── first sync (no known state) ───────────────────────────────────────────

    @Test fun `first sync both exist local newer uploads`() =
        assertEquals(SyncDecision.UPLOAD, decide(local(MS2), remote(MS)))

    @Test fun `first sync both exist remote newer downloads`() =
        assertEquals(SyncDecision.DOWNLOAD, decide(local(MS), remote(MS2)))

    @Test fun `first sync local only TWO_WAY uploads`() =
        assertEquals(SyncDecision.UPLOAD, decide(local(), null))

    @Test fun `first sync remote only TWO_WAY downloads`() =
        assertEquals(SyncDecision.DOWNLOAD, decide(null, remote()))

    // ── after upload: remote metadata null in state ───────────────────────────

    @Test fun `second sync after upload remote metadata null skips`() {
        // State saved after upload: local mtime known, remote unknown (null).
        val known = state(localMs = MS, remoteMs = null, etag = null)
        // Remote listing shows a new mtime (server assigned), but we treat null as "no change".
        assertEquals(SyncDecision.SKIP, decide(local(MS), remote(MS2), known))
    }

    @Test fun `after upload local changed again re-uploads`() {
        val known = state(localMs = MS, remoteMs = null, etag = null)
        assertEquals(SyncDecision.UPLOAD, decide(local(MS2), remote(MS2), known))
    }

    // ── after download: local mtime recorded ─────────────────────────────────

    @Test fun `second sync fully recorded skips`() {
        val known = state(localMs = MS, remoteMs = MS, etag = "abc")
        assertEquals(SyncDecision.SKIP, decide(local(MS), remote(MS, etag = "abc"), known))
    }

    @Test fun `remote changed after download downloads`() {
        val known = state(localMs = MS, remoteMs = MS, etag = "abc")
        assertEquals(SyncDecision.DOWNLOAD, decide(local(MS), remote(MS2, etag = "xyz"), known))
    }

    @Test fun `local changed after download uploads`() {
        val known = state(localMs = MS, remoteMs = MS, etag = "abc")
        assertEquals(SyncDecision.UPLOAD, decide(local(MS2), remote(MS, etag = "abc"), known))
    }

    // ── epoch-millis precision ────────────────────────────────────────────────

    @Test fun `same millisecond timestamp treated as unchanged`() {
        val ts = 1_716_393_136_789L
        assertEquals(SyncDecision.SKIP,
            decide(local(ts), remote(ts, etag = "e"), state(localMs = ts, remoteMs = ts, etag = "e")))
    }

    @Test fun `sub-second mtime difference treated as unchanged`() {
        // Second-precision comparison is intentional: FAT32 has 2s mtime resolution and WebDAV
        // 1s, so sub-second deltas are phantom changes that caused rewrite loops. A 1ms diff
        // within the same second must NOT be treated as a change.
        val ts = 1_716_393_136_789L
        assertEquals(SyncDecision.SKIP,
            decide(local(ts + 1), remote(ts, etag = "e"), state(localMs = ts, remoteMs = ts, etag = "e")))
    }

    @Test fun `mtime change of a full second detected as local change`() {
        val ts = 1_716_393_136_789L
        assertEquals(SyncDecision.UPLOAD,
            decide(local(ts + 1000), remote(ts, etag = "e"), state(localMs = ts, remoteMs = ts, etag = "e")))
    }

    @Test fun `epoch-second stored value differs from millis comparison`() {
        // If we stored 1716393136 (seconds) and compare to 1716393136000 (millis) they differ →
        // This was the original bug — now we store millis so they should match.
        val ms = 1_716_393_136_000L   // exact second boundary, no sub-second component
        assertEquals(SyncDecision.SKIP,
            decide(local(ms), remote(ms, etag = "e"), state(localMs = ms, remoteMs = ms, etag = "e")))
    }

    // ── delete behaviour ──────────────────────────────────────────────────────

    @Test fun `local exists remote deleted TWO_WAY MIRROR deletes local`() =
        assertEquals(SyncDecision.DELETE_LOCAL, decide(local(), null, state(), delete = DeleteBehavior.MIRROR))

    @Test fun `local exists remote deleted KEEP skips`() =
        assertEquals(SyncDecision.SKIP, decide(local(), null, state(), delete = DeleteBehavior.KEEP))

    @Test fun `remote deleted UPLOAD_ONLY skips local deletion`() =
        assertEquals(SyncDecision.SKIP,
            decide(local(), null, state(), dir = SyncDirection.UPLOAD_ONLY))

    @Test fun `local deleted TWO_WAY MIRROR deletes remote`() =
        assertEquals(SyncDecision.DELETE_REMOTE, decide(null, remote(), state(), delete = DeleteBehavior.MIRROR))

    @Test fun `local deleted TWO_WAY KEEP skips`() =
        assertEquals(SyncDecision.SKIP, decide(null, remote(), state(), delete = DeleteBehavior.KEEP))

    @Test fun `local deleted DOWNLOAD_ONLY skips remote deletion`() =
        assertEquals(SyncDecision.SKIP,
            decide(null, remote(), state(), dir = SyncDirection.DOWNLOAD_ONLY))

    // ── remote exists, no state record: never delete on ambiguity ────────────

    @Test fun `remote exists with no state record downloads rather than deleting`() =
        // known=null can mean a brand-new remote file OR one whose state was lost. The engine
        // cannot tell them apart, so it downloads rather than risk deleting a real file —
        // worst case is a re-downloaded file, never a lost one. A file the user genuinely
        // deleted locally still has its state record, which routes to DELETE_REMOTE.
        assertEquals(SyncDecision.DOWNLOAD,
            decide(null, remote(), known = null, delete = DeleteBehavior.MIRROR, hasPriorState = true))

    @Test fun `initial sync remote only no prior state downloads`() =
        assertEquals(SyncDecision.DOWNLOAD,
            decide(null, remote(), known = null, hasPriorState = false))

    // ── first-seen SKIP saves baseline so later deletions are detected ────────

    @Test fun `first sync both exist same mtime uploads local wins tie`() =
        assertEquals(SyncDecision.UPLOAD, decide(local(MS), remote(MS, etag = "abc")))

    @Test fun `after first-seen skip local deleted deletes remote`() {
        // Simulate: first sync saw both sides identical → SKIP (state saved by engine).
        // Then local file deleted → known is now present → DELETE_REMOTE.
        val known = state(localMs = MS, remoteMs = MS, etag = "abc")
        assertEquals(SyncDecision.DELETE_REMOTE,
            decide(null, remote(MS, etag = "abc"), known, delete = DeleteBehavior.MIRROR))
    }

    // ── directions ────────────────────────────────────────────────────────────

    @Test fun `UPLOAD_ONLY ignores remote changes`() =
        assertEquals(SyncDecision.SKIP,
            decide(local(MS), remote(MS2, etag = "new"), state(), dir = SyncDirection.UPLOAD_ONLY))

    @Test fun `DOWNLOAD_ONLY ignores local changes`() =
        assertEquals(SyncDecision.SKIP,
            decide(local(MS2), remote(MS, etag = "abc"), state(), dir = SyncDirection.DOWNLOAD_ONLY))
}
