package dev.privacyllc.period.feature.export

import android.content.Context
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContracts
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.test.core.app.ApplicationProvider
import dev.privacyllc.period.core.data.CycleData
import dev.privacyllc.period.core.data.CycleRepository
import dev.privacyllc.period.core.datastore.UserPreferencesRepository
import dev.privacyllc.period.core.notifications.NotificationCopy
import dev.privacyllc.period.domain.prediction.PersonalPredictionEngine
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
import org.robolectric.annotation.Config
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.time.Clock
import java.time.LocalDate
import java.time.ZoneOffset

/**
 * The export leaves nothing behind, and it writes where the user pointed.
 *
 * The format itself is pinned in `:core:export` against a golden file. What is
 * checked here is the part that only exists on Android: that the destination is
 * a document the *user* chose, that the bytes reach it, and — the clause from
 * `#35`'s own verify line — that no copy is left in app-private storage.
 */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class DataExporterTest {

    private val context = ApplicationProvider.getApplicationContext<Context>()
    private val today = LocalDate.of(2026, 8, 19)
    private val clock = Clock.fixed(today.atStartOfDay(ZoneOffset.UTC).toInstant(), ZoneOffset.UTC)

    private lateinit var repo: CycleRepository
    private lateinit var prefs: UserPreferencesRepository
    private lateinit var controller: ExportController
    private lateinit var exporter: DataExporter

    private val destination: Uri = Uri.parse("content://test.documents/document/42")

    @Before fun setUp() {
        repo = CycleData.repository(context, PersonalPredictionEngine(), clock)
        runBlocking { repo.deleteAllHealthData() }
        prefs = UserPreferencesRepository(
            PreferenceDataStoreFactory.create {
                context.preferencesDataStoreFile("export_test_${System.nanoTime()}")
            },
        )
        controller = ExportController()
        exporter = DataExporter(repo, prefs, controller, clock)
        context.cacheDir.deleteRecursively()
        context.cacheDir.mkdirs()
    }

    private fun await(predicate: () -> Boolean) = runBlocking {
        withTimeout(10_000) { while (!predicate()) delay(20) }
    }

    private fun registerStream(stream: OutputStream) {
        Shadows.shadowOf(context.contentResolver).registerOutputStream(destination, stream)
    }

    /**
     * The user chose the destination, and that is a property of the *intent*.
     *
     * Pinned so nobody can swap in `ACTION_SEND`, which would hand the file to
     * an app of the user's choosing rather than write it to a place of their
     * choosing — and would need a temporary copy to hand over.
     */
    @Test fun `the picker asks for a document the user names`() {
        val name = ExportCopy.fileName(today)
        val intent = ActivityResultContracts.CreateDocument("application/json")
            .createIntent(context, name)

        assertEquals(android.content.Intent.ACTION_CREATE_DOCUMENT, intent.action)
        assertEquals("application/json", intent.type)
        assertEquals(name, intent.getStringExtra(android.content.Intent.EXTRA_TITLE))
    }

    @Test fun `the file name says nothing about what the app is for`() {
        val name = ExportCopy.fileName(today)
        val leaks = NotificationCopy.SENSITIVE_WORDS.filter { name.contains(it, ignoreCase = true) }
        assertEquals("the file name is visible in pickers and sync notifications", emptyList<String>(), leaks)
        assertTrue(name.endsWith(".json"))
    }

    @Test fun `the export reaches the stream the resolver hands back`() {
        runBlocking { repo.confirmPeriodStart(LocalDate.of(2026, 7, 16)) }
        val sink = ByteArrayOutputStream()
        registerStream(sink)

        exporter.export(context, destination)
        await { controller.outcome.value == ExportOutcome.DONE }

        val written = sink.toString(Charsets.UTF_8.name())
        assertTrue("no document was written", written.contains("\"format\""))
        assertTrue("the user's record is missing", written.contains("2026-07-16"))
        assertTrue("a derived value leaked into the file", !written.contains("confidence"))
    }

    /**
     * `#35`'s verify line, made mechanical: *"with no copy left in app-private
     * or external storage afterwards"*.
     *
     * This is the cheapest possible proof, and it fails the moment anybody
     * reintroduces the write-then-share pattern, because that needs a real file
     * in `cacheDir`.
     */
    @Test fun `no copy is left anywhere in app-private storage`() {
        runBlocking { repo.confirmPeriodStart(LocalDate.of(2026, 7, 16)) }
        registerStream(ByteArrayOutputStream())

        exporter.export(context, destination)
        await { controller.outcome.value == ExportOutcome.DONE }

        val leftovers = context.cacheDir.walkTopDown().filter { it.isFile }.toList()
        assertEquals("the export left a temporary file behind", emptyList<java.io.File>(), leftovers)

        val inFiles = context.filesDir.walkTopDown()
            .filter { it.isFile && it.name.contains("records") }
            .toList()
        assertEquals(emptyList<java.io.File>(), inFiles)
    }

    /**
     * A stream that dies partway must not be reported as a finished export.
     * "Saved" over a truncated file is the worst outcome this feature has.
     */
    @Test fun `a failing stream is reported as a failure, never as done`() {
        runBlocking { repo.confirmPeriodStart(LocalDate.of(2026, 7, 16)) }
        registerStream(object : OutputStream() {
            override fun write(b: Int) = throw java.io.IOException("disk full")
            override fun write(b: ByteArray, off: Int, len: Int) = throw java.io.IOException("disk full")
        })

        exporter.export(context, destination)
        await { controller.outcome.value != ExportOutcome.WRITING }

        assertFalse(
            "a failed write was reported as a completed export",
            controller.outcome.value == ExportOutcome.DONE,
        )
    }

    /**
     * The destination is consumed once.
     *
     * A relock/unlock cycle re-enters the branch that drains it, and writing a
     * second time into a document the user already received would change a file
     * they believed was finished.
     */
    @Test fun `a destination is taken once and then gone`() {
        controller.onDestination(destination)
        assertEquals(destination, controller.takeDestination())
        assertEquals(null, controller.takeDestination())
    }

    @Test fun `cancelling the picker says nothing at all`() {
        controller.onDestination(null)
        assertEquals(ExportOutcome.IDLE, controller.outcome.value)
        assertEquals(null, controller.takeDestination())
    }
}
