package com.syncflow.domain.sync

import com.syncflow.domain.model.ConflictStrategy
import com.syncflow.domain.model.DeleteBehavior
import com.syncflow.domain.model.ScheduleType
import com.syncflow.domain.model.SyncDirection
import com.syncflow.domain.model.SyncPair
import com.syncflow.domain.model.SyncStatus
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * Exclusion symmetry: paths excluded from the local walk must also be excluded from the remote
 * listing and the merged work set, so a previously-uploaded excluded file is never DELETE_REMOTE'd
 * in an endless churn loop (the ".thumbnails" cache regression).
 */
class ExcludePathTest {

    private fun pair(
        excludePatterns: List<String> = emptyList(),
        excludeExtensions: List<String> = emptyList(),
        skipHiddenFiles: Boolean = false,
    ) = SyncPair(
        id = 1, name = "t", localPath = "/l", remotePath = "/r", accountId = 1,
        syncDirection = SyncDirection.TWO_WAY,
        conflictStrategy = ConflictStrategy.KEEP_NEWEST,
        deleteBehavior = DeleteBehavior.MIRROR,
        recursive = true,
        scheduleType = ScheduleType.MANUAL, scheduleIntervalMinutes = 0,
        scheduleDailyTime = null, scheduleWeekdays = 0,
        wifiOnly = false, wifiSsid = "", chargingOnly = false, minBatteryPct = 0,
        excludePatterns = excludePatterns, includeExtensions = emptyList(),
        excludeExtensions = excludeExtensions, skipHiddenFiles = skipHiddenFiles,
        minFileSizeKb = 0, maxFileSizeKb = 0,
        notifyOnComplete = false, notifyOnError = false,
        isEnabled = true, lastSyncAt = null, lastSyncResult = SyncStatus.IDLE,
        pendingConflicts = 0,
    )

    @Test fun `thumbnails cache is always excluded regardless of config`() {
        val p = pair()
        assertTrue(isExcludedPath("DCIM/.thumbnails/123.jpg", p))
        assertTrue(isExcludedPath("Pictures/.thumbnails/1000020397.jpg", p))
        assertTrue(isExcludedPath(".thumbnails/x.jpg", p))
    }

    @Test fun `android trash and pending staging dirs are excluded`() {
        val p = pair()
        assertTrue(isExcludedPath("DCIM/Camera/.trashed-1700000000-IMG_0001.jpg", p))
        assertTrue(isExcludedPath("DCIM/.pending-1700000000-VID_0001.mp4", p))
    }

    @Test fun `our own atomic-write temp files are excluded`() {
        val p = pair()
        assertTrue(isExcludedPath("Download/.movie.mp4.sfpart", p))
        assertTrue(isExcludedPath("a/b/.photo.jpg.sfpart", p))
    }

    @Test fun `android app-private trees are excluded (scoped-storage unreadable)`() {
        val p = pair()
        assertTrue(isExcludedPath("Android/media/com.whatsapp/WhatsApp/Media/IMG.jpg", p))
        assertTrue(isExcludedPath("Android/data/com.foo/files/x.bin", p))
        assertTrue(isExcludedPath("Android/obb/com.game/main.obb", p))
        assertTrue(isExcludedPath("android/MEDIA/com.x/y.jpg", p)) // case-insensitive
    }

    @Test fun `non-private Android paths are not excluded`() {
        val p = pair()
        // A user folder literally named "Android" at a deeper level is fine; only the
        // top-level app-private trees are blocked.
        assertFalse(isExcludedPath("DCIM/Android/holiday.jpg", p))
        assertFalse(isExcludedPath("Pictures/android-wallpaper.png", p))
    }

    @Test fun `normal media files are not excluded`() {
        val p = pair()
        assertFalse(isExcludedPath("DCIM/Camera/IMG_0001.jpg", p))
        assertFalse(isExcludedPath("Pictures/cat.png", p))
        assertFalse(isExcludedPath("اصفهان/20171023.jpg", p)) // unicode folder, real data
    }

    @Test fun `user exclude patterns and extensions still apply on filename`() {
        assertTrue(isExcludedPath("a/Thumbs.db", pair(excludePatterns = listOf("Thumbs.db"))))
        assertTrue(isExcludedPath("a/note.tmp", pair(excludePatterns = listOf("*.tmp"))))
        assertTrue(isExcludedPath("a/data.log", pair(excludeExtensions = listOf("log"))))
        assertFalse(isExcludedPath("a/keep.jpg", pair(excludeExtensions = listOf("log"))))
    }

    @Test fun `skipHiddenFiles excludes dotfiles by filename only`() {
        assertTrue(isExcludedPath("a/.hidden", pair(skipHiddenFiles = true)))
        assertFalse(isExcludedPath("a/.hidden", pair(skipHiddenFiles = false)))
    }

    @Test fun `remote-not-found detected from http status messages`() {
        assertTrue(isRemoteNotFound(Exception("HTTP 404")))
        assertTrue(isRemoteNotFound(Exception("Not Found")))
        assertTrue(isRemoteNotFound(RuntimeException("PROPFIND failed: notfound")))
    }

    @Test fun `remote-not-found is false for other errors`() {
        assertFalse(isRemoteNotFound(Exception("HTTP 500")))
        assertFalse(isRemoteNotFound(Exception("timeout")))
        assertFalse(isRemoteNotFound(Exception(null as String?)))
    }
}
