package app.closer.data.repository

import app.closer.domain.model.LocalAnswer
import app.closer.domain.repository.LocalAnswerRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

/**
 * Tests the answer save/reveal state machine via [FakeLocalAnswerRepository].
 *
 * [SharedPreferencesLocalAnswerRepository] depends on Android's EncryptedSharedPreferences
 * and cannot run in a JVM unit test. [FakeLocalAnswerRepository] mirrors the identical
 * business logic without the platform dependency, letting us verify:
 *   - save creates / updates answers
 *   - save preserves `createdAt` and `isRevealed` on re-save (critical invariant)
 *   - `markRevealed` transitions isRevealed false → true without mutating other fields
 *   - `delete` removes the answer
 */
class LocalAnswerStateTest {

    private lateinit var repo: FakeLocalAnswerRepository

    @Before
    fun setup() {
        repo = FakeLocalAnswerRepository()
    }

    @Test
    fun `saveAnswer stores a new answer`() = runTest {
        repo.saveAnswer(answer("q1"))
        assertNotNull(repo.getAnswer("q1"))
    }

    @Test
    fun `saveAnswer preserves createdAt on re-save`() = runTest {
        repo.saveAnswer(answer("q1", createdAt = 1000L))
        // Second save with a different createdAt — must keep the original
        repo.saveAnswer(answer("q1", createdAt = 9999L))
        assertEquals(1000L, repo.getAnswer("q1")?.createdAt)
    }

    @Test
    fun `saveAnswer preserves isRevealed flag on re-save`() = runTest {
        repo.saveAnswer(answer("q1"))
        repo.markRevealed("q1")
        assertTrue(repo.getAnswer("q1")?.isRevealed == true)

        // Re-saving the same answer must not reset the revealed flag
        repo.saveAnswer(answer("q1", isRevealed = false))
        assertTrue(
            "isRevealed must survive a re-save",
            repo.getAnswer("q1")?.isRevealed == true
        )
    }

    @Test
    fun `saveAnswer updates written text on re-save`() = runTest {
        repo.saveAnswer(answer("q1", writtenText = "first draft"))
        repo.saveAnswer(answer("q1", writtenText = "revised"))
        assertEquals("revised", repo.getAnswer("q1")?.writtenText)
    }

    @Test
    fun `markRevealed transitions answer to revealed`() = runTest {
        repo.saveAnswer(answer("q1", isRevealed = false))
        assertFalse(repo.getAnswer("q1")?.isRevealed == true)

        repo.markRevealed("q1")

        assertTrue(repo.getAnswer("q1")?.isRevealed == true)
    }

    @Test
    fun `markRevealed on non-existent question is a no-op`() = runTest {
        // Should not throw
        repo.markRevealed("nonexistent")
        assertNull(repo.getAnswer("nonexistent"))
    }

    @Test
    fun `deleteAnswer removes the answer`() = runTest {
        repo.saveAnswer(answer("q1"))
        assertNotNull(repo.getAnswer("q1"))

        repo.deleteAnswer("q1")
        assertNull(repo.getAnswer("q1"))
    }

    @Test
    fun `deleteAnswer on non-existent question is a no-op`() = runTest {
        repo.deleteAnswer("nonexistent") // must not throw
    }

    @Test
    fun `observeAnswer emits updated state after save and reveal`() = runTest {
        assertNull(repo.getAnswer("q2"))

        repo.saveAnswer(answer("q2"))
        val saved = repo.getAnswer("q2")
        assertNotNull(saved)
        assertFalse(saved?.isRevealed == true)

        repo.markRevealed("q2")
        assertTrue(repo.getAnswer("q2")?.isRevealed == true)
    }

    // ── helpers ──────────────────────────────────────────────────────────────

    private fun answer(
        questionId: String,
        isRevealed: Boolean = false,
        createdAt: Long = 1_000L,
        writtenText: String = "Some answer"
    ) = LocalAnswer(
        questionId = questionId,
        questionText = "Question $questionId",
        category = "test",
        answerType = "written",
        writtenText = writtenText,
        createdAt = createdAt,
        isRevealed = isRevealed
    )
}

// ── Test double ───────────────────────────────────────────────────────────────

/**
 * In-memory [LocalAnswerRepository] that mirrors the exact save/reveal algorithm
 * of [SharedPreferencesLocalAnswerRepository] without requiring Android platform APIs.
 *
 * Any change to the real repository's logic should be reflected here too.
 */
class FakeLocalAnswerRepository : LocalAnswerRepository {

    private val _answers = MutableStateFlow<List<LocalAnswer>>(emptyList())

    override fun observeAnswers(): Flow<List<LocalAnswer>> = _answers

    override fun observeAnswer(questionId: String): Flow<LocalAnswer?> =
        _answers.map { list -> list.firstOrNull { it.questionId == questionId } }

    override suspend fun getAnswer(questionId: String): LocalAnswer? =
        _answers.value.firstOrNull { it.questionId == questionId }

    override suspend fun saveAnswer(answer: LocalAnswer) {
        val existing = _answers.value.firstOrNull { it.questionId == answer.questionId }
        val saved = answer.copy(
            createdAt = existing?.createdAt ?: answer.createdAt,
            updatedAt = System.currentTimeMillis(),
            isRevealed = existing?.isRevealed ?: answer.isRevealed
        )
        _answers.value = _answers.value
            .filterNot { it.questionId == saved.questionId }
            .plus(saved)
            .sortedByDescending { it.updatedAt }
    }

    override suspend fun markRevealed(questionId: String) {
        _answers.value = _answers.value.map { answer ->
            if (answer.questionId == questionId) {
                answer.copy(isRevealed = true, updatedAt = System.currentTimeMillis())
            } else {
                answer
            }
        }
    }

    override suspend fun deleteAnswer(questionId: String) {
        _answers.value = _answers.value.filterNot { it.questionId == questionId }
    }
}
