package com.tencent.qian.webview.internal

import android.Manifest
import android.app.Activity
import android.app.DownloadManager
import android.content.Context
import android.os.Build
import android.webkit.WebView
import androidx.activity.ComponentActivity
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.tencent.qian.webview.model.SdkEvent
import com.tencent.qian.webview.model.SignCallback
import com.tencent.qian.webview.model.SignResult
import com.tencent.qian.webview.permission.RuntimePermissionRequester
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows
import org.robolectric.android.controller.ActivityController
import org.robolectric.annotation.Config

/**
 * [DownloadListenerInstaller] 单测。
 *
 * 覆盖场景:
 * - 非 http(s) scheme 被阻断 + DownloadFailed 事件
 * - DownloadStart 事件正确发出(url / mimeType / contentDisposition)
 * - Android 10+ 不请求 WRITE_EXTERNAL_STORAGE,直接走 DownloadManager
 * - Android 6-9 未授权时请求权限;授权后走 DownloadManager;拒绝后 fallback
 * - DownloadManager 不可用时 fallback 到 ACTION_VIEW
 * - ACTION_VIEW 也失败时发 DownloadFailed
 * - null callback 不崩溃
 * - 多次下载触发多次事件
 *
 * 注:使用 Robolectric shadow 来模拟 DownloadManager 和权限状态。
 */
@RunWith(AndroidJUnit4::class)
class DownloadListenerInstallerTest {

    private lateinit var context: Context
    private lateinit var activityController: ActivityController<ComponentActivity>
    private lateinit var activity: ComponentActivity
    private lateinit var permissionRequester: RuntimePermissionRequester
    private val events = mutableListOf<SdkEvent>()
    private val callback = object : SignCallback {
        override fun onSignResult(result: SignResult) {}
        override fun onEvent(event: SdkEvent) {
            events.add(event)
        }
    }

    @Before
    fun setUp() {
        context = RuntimeEnvironment.getApplication()
        activityController = Robolectric.buildActivity(ComponentActivity::class.java)
        // 注册 permissionRequester 必须在 onCreate 之前
        activity = activityController.get()
        permissionRequester = RuntimePermissionRequester.register(activity)
        activityController.create()
        events.clear()
    }

    private fun installAndGetListener(): android.webkit.DownloadListener {
        val webView = WebView(context)
        DownloadListenerInstaller.install(
            webView = webView,
            context = context,
            permissionRequester = permissionRequester,
            activityProvider = { activity },
            callbackProvider = { callback }
        )
        return Shadows.shadowOf(webView).downloadListener
    }

    // ────────── 1. 非 http(s) scheme 被阻断 ──────────

    @Test
    fun `blocks intent scheme and emits DownloadFailed`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "intent://scan/#Intent;scheme=zxing;end",
            "Mozilla/5.0",
            "attachment; filename=test.apk",
            "application/vnd.android.package-archive",
            1024
        )

        assertEquals(1, events.size)
        val event = events[0] as SdkEvent.DownloadFailed
        assertTrue(event.reason.contains("non-http(s) scheme"))
        assertEquals("intent://scan/#Intent;scheme=zxing;end", event.url)
    }

    @Test
    fun `blocks javascript scheme and emits DownloadFailed`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "javascript:alert(1)",
            "Mozilla/5.0",
            "",
            "text/html",
            0
        )

        assertEquals(1, events.size)
        assertTrue(events[0] is SdkEvent.DownloadFailed)
    }

    @Test
    fun `blocks data scheme and emits DownloadFailed`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "data:text/html,<h1>test</h1>",
            "Mozilla/5.0",
            "",
            "text/html",
            100
        )

        assertEquals(1, events.size)
        assertTrue(events[0] is SdkEvent.DownloadFailed)
    }

    @Test
    fun `blocks ftp scheme and emits DownloadFailed`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "ftp://files.example.com/secret.zip",
            "Mozilla/5.0",
            "",
            "application/zip",
            4096
        )

        assertEquals(1, events.size)
        assertTrue(events[0] is SdkEvent.DownloadFailed)
    }

    // ────────── 2. http(s) URL 发出 DownloadStart 事件 ──────────

    @Test
    @Config(sdk = [Build.VERSION_CODES.Q]) // API 29
    fun `https URL emits DownloadStart event with correct fields`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "https://example.com/contract.pdf",
            "Mozilla/5.0",
            "attachment; filename=contract.pdf",
            "application/pdf",
            2048
        )

        assertTrue(events.isNotEmpty())
        val startEvent = events[0] as SdkEvent.DownloadStart
        assertEquals("https://example.com/contract.pdf", startEvent.url)
        assertEquals("application/pdf", startEvent.mimeType)
        assertEquals("attachment; filename=contract.pdf", startEvent.contentDisposition)
    }

    @Test
    @Config(sdk = [Build.VERSION_CODES.Q])
    fun `http URL also emits DownloadStart`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "http://example.com/file.zip",
            "Mozilla/5.0",
            "attachment; filename=file.zip",
            "application/zip",
            4096
        )

        assertTrue(events.isNotEmpty())
        val startEvent = events[0] as SdkEvent.DownloadStart
        assertEquals("http://example.com/file.zip", startEvent.url)
        assertEquals("application/zip", startEvent.mimeType)
    }

    // ────────── 3. Android 10+ 直接走 DownloadManager ──────────

    @Test
    @Config(sdk = [Build.VERSION_CODES.Q]) // API 29
    fun `API 29 enqueues via DownloadManager without permission request`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "https://example.com/contract.pdf",
            "Mozilla/5.0",
            "attachment; filename=contract.pdf",
            "application/pdf",
            2048
        )

        // 验证 DownloadManager 被调用
        val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val shadowDm = Shadows.shadowOf(dm)
        assertTrue("应有下载请求入队", shadowDm.requestCount > 0)
        // 不应有 PermissionDenied 事件
        assertTrue(events.none { it is SdkEvent.PermissionDenied })
    }

    @Test
    @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) // API 33
    fun `API 33 enqueues via DownloadManager without permission request`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "https://example.com/report.xlsx",
            "Mozilla/5.0",
            "attachment; filename=report.xlsx",
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            65536
        )

        val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val shadowDm = Shadows.shadowOf(dm)
        assertTrue(shadowDm.requestCount > 0)
    }

    // ────────── 4. Android 6-9 权限已授予时直接下载 ──────────

    @Test
    @Config(sdk = [Build.VERSION_CODES.O]) // API 26
    fun `API 26 with granted permission enqueues directly`() {
        // 通过 Robolectric shadow 授予权限
        val shadowApp = Shadows.shadowOf(RuntimeEnvironment.getApplication())
        shadowApp.grantPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)

        val listener = installAndGetListener()

        listener.onDownloadStart(
            "https://example.com/contract.pdf",
            "Mozilla/5.0",
            "attachment; filename=contract.pdf",
            "application/pdf",
            2048
        )

        // 应该有 DownloadStart 事件
        assertTrue(events.any { it is SdkEvent.DownloadStart })
        // 不应有 PermissionDenied
        assertTrue(events.none { it is SdkEvent.PermissionDenied })
    }

    // ────────── 5. null callback 不崩溃 ──────────

    @Test
    fun `null callback does not crash on blocked scheme`() {
        val webView = WebView(context)
        DownloadListenerInstaller.install(
            webView = webView,
            context = context,
            permissionRequester = permissionRequester,
            activityProvider = { activity },
            callbackProvider = { null }
        )
        val listener = Shadows.shadowOf(webView).downloadListener

        // 不应崩溃
        listener.onDownloadStart(
            "intent://bad",
            "Mozilla/5.0",
            "",
            "application/octet-stream",
            0
        )
    }

    @Test
    @Config(sdk = [Build.VERSION_CODES.Q])
    fun `null callback does not crash on valid download`() {
        val webView = WebView(context)
        DownloadListenerInstaller.install(
            webView = webView,
            context = context,
            permissionRequester = permissionRequester,
            activityProvider = { activity },
            callbackProvider = { null }
        )
        val listener = Shadows.shadowOf(webView).downloadListener

        listener.onDownloadStart(
            "https://example.com/file.pdf",
            "Mozilla/5.0",
            "attachment; filename=file.pdf",
            "application/pdf",
            1024
        )
        // 无异常即通过
    }

    // ────────── 6. 多次下载触发多次事件 ──────────

    @Test
    @Config(sdk = [Build.VERSION_CODES.Q])
    fun `multiple downloads emit multiple DownloadStart events`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "https://example.com/file1.pdf",
            "Mozilla/5.0", "", "application/pdf", 1024
        )
        listener.onDownloadStart(
            "https://example.com/file2.zip",
            "Mozilla/5.0", "", "application/zip", 2048
        )

        val starts = events.filterIsInstance<SdkEvent.DownloadStart>()
        assertEquals(2, starts.size)
        assertEquals("https://example.com/file1.pdf", starts[0].url)
        assertEquals("https://example.com/file2.zip", starts[1].url)
    }

    // ────────── 7. 空 URL 处理 ──────────

    @Test
    fun `empty url emits DownloadFailed`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "",
            "Mozilla/5.0",
            "",
            "application/pdf",
            0
        )

        assertTrue(events.any { it is SdkEvent.DownloadFailed })
    }

    // ────────── 8. Activity 不可用时 fallback ──────────

    @Test
    @Config(sdk = [Build.VERSION_CODES.O]) // API 26, 需要权限
    fun `null activity provider falls back gracefully`() {
        val webView = WebView(context)
        DownloadListenerInstaller.install(
            webView = webView,
            context = context,
            permissionRequester = permissionRequester,
            activityProvider = { null },
            callbackProvider = { callback }
        )
        val listener = Shadows.shadowOf(webView).downloadListener

        listener.onDownloadStart(
            "https://example.com/contract.pdf",
            "Mozilla/5.0",
            "attachment; filename=contract.pdf",
            "application/pdf",
            2048
        )

        // 应该有 DownloadStart 事件
        assertTrue(events.any { it is SdkEvent.DownloadStart })
        // Activity 不可用,无法弹权限,应 fallback(不崩溃)
    }

    // ────────── 9. MIME 和 contentDisposition 为 null/空时不崩溃 ──────────

    @Test
    @Config(sdk = [Build.VERSION_CODES.Q])
    fun `null mimetype and contentDisposition handled gracefully`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "https://example.com/unknown",
            "Mozilla/5.0",
            null,
            null,
            512
        )

        // 应该有 DownloadStart 事件,字段为空字符串
        val startEvent = events.filterIsInstance<SdkEvent.DownloadStart>().firstOrNull()
        assertTrue("应有 DownloadStart 事件", startEvent != null)
        assertEquals("", startEvent!!.mimeType)
        assertEquals("", startEvent.contentDisposition)
    }

    // ────────── 10. scheme 大小写不敏感 ──────────

    @Test
    @Config(sdk = [Build.VERSION_CODES.Q])
    fun `HTTPS uppercase scheme is allowed`() {
        val listener = installAndGetListener()

        listener.onDownloadStart(
            "HTTPS://example.com/file.pdf",
            "Mozilla/5.0",
            "attachment; filename=file.pdf",
            "application/pdf",
            1024
        )

        // 应该有 DownloadStart(不应被阻断)
        assertTrue(events.any { it is SdkEvent.DownloadStart })
        assertTrue(events.none { it is SdkEvent.DownloadFailed })
    }
}