package dev.m1sk9.lunaticChat.paper.converter

import dev.m1sk9.lunaticChat.paper.TestUtils
import dev.m1sk9.lunaticChat.paper.converter.GoogleIMEClient
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIsNot
import kotlin.test.assertNull
import kotlin.time.Duration.Companion.seconds

/**
 * Tests for RomanjiConverter.
 * Verifies conversion logic, English word handling, and caching behavior.
 *
 * Note: The alphabetic character check after KanaConverter determines whether
 * a word is treated as English or romaji:
 * - "version" -> "vえrしおん" (has 'v', 'r') -> kept as "version"
 * - "hello" -> "へっぉ" (no alphabetic chars) -> treated as romaji, converted
 * - "API" -> "あぴ" (no alphabetic chars) -> treated as romaji, converted
 */
class RomanjiConverterTest {
    private fun createConverter(debugMode: Boolean = false): Triple<RomanjiConverter, ConversionCache, GoogleIMEClient> {
        val cache = mockk<ConversionCache>(relaxed = true)
        val apiClient = mockk<GoogleIMEClient>(relaxed = true)
        val logger = TestUtils.TestLogger()

        every { cache.get(any()) } returns null

        val converter = RomanjiConverter(cache, apiClient, logger, debugMode)
        return Triple(converter, cache, apiClient)
    }

    // ===== English words with unconvertible letters should NOT be converted =====
    // Words containing letters like 'v', 'l', 'x', 'q' that have no direct hiragana mapping
    // will retain those letters after KanaConverter, triggering the English word detection.

    @Test
    fun `English word 'version' should not be converted (has v and r)`() =
        runBlocking {
            val (converter, _, _) = createConverter()
            // "version" -> "vえrしおん" (v, r remain)

            val result = converter.convert("version")

            assertEquals("version", result)
        }

    @Test
    fun `English word 'server' should not be converted (has r and v)`() =
        runBlocking {
            val (converter, _, _) = createConverter()
            // "server" -> "せrvえr" (r, v, r remain)

            val result = converter.convert("server")

            assertEquals("server", result)
        }

    @Test
    fun `English word 'latest' should not be converted (has s and t)`() =
        runBlocking {
            val (converter, _, _) = createConverter()
            // "latest" -> "ぁてst" (s, t remain)

            val result = converter.convert("latest")

            assertEquals("latest", result)
        }

    @Test
    fun `English word 'running' should not be converted (has g)`() =
        runBlocking {
            val (converter, _, _) = createConverter()
            // "running" -> "るんいんg" (g remains)

            val result = converter.convert("running")

            assertEquals("running", result)
        }

    @Test
    fun `English word 'world' should not be converted (has r, l, d)`() =
        runBlocking {
            val (converter, _, _) = createConverter()
            // "world" -> "をrld" (r, l, d remain)

            val result = converter.convert("world")

            assertEquals("world", result)
        }

    @Test
    fun `English words should not call API`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()

            // These words have unconvertible letters
            converter.convert("version")
            converter.convert("server")
            converter.convert("world")

            coVerify(exactly = 0) { apiClient.convert(any()) }
        }

    // ===== Words that happen to be valid romaji will be converted =====
    // Some English words like "hello" (h->へ, ll->っ, o->ぉ) or "API" (a->あ, p->ぴ, i is consumed)
    // completely convert to hiragana, so they are treated as romaji.

    @Test
    fun `'hello' converts completely to hiragana so it is treated as romaji`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()
            // "hello" -> "へっぉ" (no alphabetic chars remain)
            coEvery { apiClient.convert("へっぉ") } returns "へっぉ"

            val result = converter.convert("hello")

            assertEquals("へっぉ", result)
            coVerify(exactly = 1) { apiClient.convert("へっぉ") }
        }

    @Test
    fun `'API' converts completely to hiragana so it is treated as romaji`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()
            // "API" -> "あぴ" (no alphabetic chars remain)
            coEvery { apiClient.convert("あぴ") } returns "あぴ"

            val result = converter.convert("API")

            assertEquals("あぴ", result)
            coVerify(exactly = 1) { apiClient.convert("あぴ") }
        }

    // ===== Pure romaji should be converted =====

    @Test
    fun `romaji 'konnichiwa' should be converted`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()
            // KanaConverter.toHiragana("konnichiwa") returns "こんいちわ"
            coEvery { apiClient.convert("こんいちわ") } returns "こんにちは"

            val result = converter.convert("konnichiwa")

            assertEquals("こんにちは", result)
            coVerify(exactly = 1) { apiClient.convert("こんいちわ") }
        }

    @Test
    fun `romaji 'ohayou' should be converted`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()
            coEvery { apiClient.convert("おはよう") } returns "おはよう"

            val result = converter.convert("ohayou")

            assertEquals("おはよう", result)
            coVerify(exactly = 1) { apiClient.convert("おはよう") }
        }

    @Test
    fun `romaji 'arigatou' should be converted`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()
            coEvery { apiClient.convert("ありがとう") } returns "ありがとう"

            val result = converter.convert("arigatou")

            assertEquals("ありがとう", result)
            coVerify(exactly = 1) { apiClient.convert("ありがとう") }
        }

    // ===== Mixed text (romaji + English) =====

    @Test
    fun `mixed text should convert romaji and keep English words with unconvertible letters`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()
            // "konnichiwa" -> "こんいちわ" (pure romaji, converted)
            // "server" -> "せrvえr" (has r, v, r, kept as English)
            coEvery { apiClient.convert("こんいちわ") } returns "こんにちは"

            val result = converter.convert("konnichiwa server")

            assertEquals("こんにちは server", result)
            coVerify(exactly = 1) { apiClient.convert("こんいちわ") }
        }

    @Test
    fun `multiple English words with unconvertible letters should all be preserved`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()
            // "version" has v, r
            // "server" has r, v, r
            // "world" has r, l, d

            val result = converter.convert("version server world")

            assertEquals("version server world", result)
            coVerify(exactly = 0) { apiClient.convert(any()) }
        }

    // ===== Edge cases =====

    @Test
    fun `empty string should return empty string`() =
        runBlocking {
            val (converter, _, _) = createConverter()

            val result = converter.convert("")

            assertEquals("", result)
        }

    @Test
    fun `non-ASCII input should return null`() =
        runBlocking {
            val (converter, _, _) = createConverter()

            val result = converter.convert("こんにちは")

            assertNull(result)
        }

    @Test
    fun `numbers only should return as-is`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()
            // Numbers pass through KanaConverter unchanged, no alphabetic chars
            coEvery { apiClient.convert("123") } returns "123"

            val result = converter.convert("123")

            assertEquals("123", result)
        }

    @Test
    fun `spaces only should return empty due to word filtering`() =
        runBlocking {
            val (converter, _, _) = createConverter()

            val result = converter.convert("   ")

            assertEquals("", result)
        }

    // ===== Cache behavior =====

    @Test
    fun `cached word should be retrieved from cache`() =
        runBlocking {
            val (converter, cache, apiClient) = createConverter()
            every { cache.get("konnichiwa") } returns "こんにちは"

            val result = converter.convert("konnichiwa")

            assertEquals("こんにちは", result)
            verify(exactly = 1) { cache.get("konnichiwa") }
            coVerify(exactly = 0) { apiClient.convert(any()) }
        }

    @Test
    fun `converted word should be stored in cache`() =
        runBlocking {
            val (converter, cache, apiClient) = createConverter()
            coEvery { apiClient.convert("おはよう") } returns "おはよう"

            converter.convert("ohayou")

            verify(exactly = 1) { cache.put("ohayou", "おはよう") }
        }

    @Test
    fun `English words with unconvertible letters should not be cached`() =
        runBlocking {
            val (converter, cache, _) = createConverter()

            converter.convert("version")

            verify(exactly = 0) { cache.put(any(), any()) }
        }

    // ===== API error handling =====

    @Test
    fun `a conversion timeout is not cancellation`() {
        // Reported through the cancellation channel, a slow request reached the delivery queue
        // looking like shutdown and ended the worker, so the sender's later messages were buffered
        // and never delivered.
        assertIsNot<CancellationException>(ConversionTimeoutException(1.seconds))
    }

    @Test
    fun `an API timeout degrades to hiragana without caching it`() =
        runBlocking {
            val (converter, cache, apiClient) = createConverter()
            coEvery { apiClient.convert("おはよう") } throws ConversionTimeoutException(1.seconds)

            val result = converter.convert("ohayou")

            assertEquals("おはよう", result)
            // A slow reply says nothing about the word, so caching the hiragana would pin it to its
            // unconverted form for the life of the cache.
            verify(exactly = 0) { cache.put(any(), any()) }
        }

    @Test
    fun `a word that timed out is converted on the next attempt`() =
        runBlocking {
            // A cache that actually remembers, unlike the shared fixture whose get() always returns
            // null - which would let this pass even if the timeout had been cached.
            val entries = mutableMapOf<String, String>()
            val cache = mockk<ConversionCache>(relaxed = true)
            every { cache.get(any()) } answers { entries[firstArg()] }
            every { cache.put(any(), any()) } answers { entries[firstArg()] = secondArg() }
            val apiClient = mockk<GoogleIMEClient>(relaxed = true)
            val converter = RomanjiConverter(cache, apiClient, TestUtils.TestLogger())

            coEvery { apiClient.convert("おはよう") } throws ConversionTimeoutException(1.seconds)
            converter.convert("ohayou")
            coEvery { apiClient.convert("おはよう") } returns "おはよう御座います"

            assertEquals("おはよう御座います", converter.convert("ohayou"))
        }

    @Test
    fun `API failure should fallback to hiragana`() =
        runBlocking {
            val (converter, cache, apiClient) = createConverter()
            coEvery { apiClient.convert("おはよう") } throws Exception("API Error")

            val result = converter.convert("ohayou")

            assertEquals("おはよう", result)
            verify(exactly = 1) { cache.put("ohayou", "おはよう") }
        }

    @Test
    fun `words in one message are converted concurrently`() =
        runBlocking {
            val (converter, _, apiClient) = createConverter()
            val inFlight = AtomicInteger(0)
            val peakInFlight = AtomicInteger(0)

            coEvery { apiClient.convert(any()) } coAnswers {
                peakInFlight.updateAndGet { maxOf(it, inFlight.incrementAndGet()) }
                delay(50)
                inFlight.decrementAndGet()
                "変換"
            }

            converter.convert("konnichiwa ohayou arigatou")

            assertEquals(3, peakInFlight.get(), "each word should be in flight at the same time")
        }

    @Test
    fun `a message-level timeout caches nothing`() =
        runBlocking {
            val (converter, cache, apiClient) = createConverter()
            coEvery { apiClient.convert(any()) } coAnswers {
                delay(1_000)
                "変換"
            }

            val result = withTimeoutOrNull(100) { converter.convert("konnichiwa ohayou arigatou") }

            assertNull(result)
            // The words share one timeout, so caching the hiragana fallback here would pin the whole
            // message - not just one word - to its unconverted form for the life of the cache.
            verify(exactly = 0) { cache.put(any(), any()) }
        }
}
