/*
 * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package test.io.encoding

import kotlin.test.*
import kotlin.io.encoding.Base64
import kotlin.io.encoding.Base64.PaddingOption.*

class Base64Test {

    private fun Base64.PaddingOption.isPresentOnEncode(): Boolean =
        this == PRESENT || this == PRESENT_OPTIONAL

    private fun Base64.PaddingOption.isOptionalOnDecode(): Boolean =
        this == PRESENT_OPTIONAL || this == ABSENT_OPTIONAL

    private fun Base64.PaddingOption.isAllowedOnDecode(): Boolean =
        this == PRESENT || isOptionalOnDecode()

    private fun testEncode(codec: Base64, bytes: ByteArray, expected: String) {
        assertEquals(expected, codec.encode(bytes))
        assertContentEquals(expected.encodeToByteArray(), codec.encodeToByteArray(bytes))
    }

    private fun testDecode(codec: Base64, symbols: String, expected: ByteArray) {
        assertContentEquals(expected, codec.decode(symbols))
        assertContentEquals(expected, codec.decode(symbols.encodeToByteArray()))
    }

    private fun testCoding(codec: Base64, bytes: ByteArray, symbols: String) {
        testEncode(codec, bytes, symbols)
        testDecode(codec, symbols, bytes)
    }

    private fun bytes(vararg values: Int): ByteArray {
        return ByteArray(values.size) { values[it].toByte() }
    }

    private val codecs = listOf(
        Base64 to "Basic",
        Base64.UrlSafe to "UrlSafe",
        Base64.Mime to "Mime",
        Base64.Pem to "PEM",
    )

    @Test
    fun index() {
        val bytes = bytes(0b0000_0100, 0b0010_0000, 0b1100_0100, 0b0001_0100, 0b0110_0001, 0b1100_1000)
        val symbols = "BCDEFGHI"

        // encode
        for ((base64, scheme) in codecs) {
            testEncode(base64, bytes, symbols)
            assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.encode(bytes, startIndex = -1) }
            assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.encode(bytes, endIndex = bytes.size + 1) }
            assertFailsWith<IllegalArgumentException>(scheme) { base64.encode(bytes, startIndex = bytes.size + 1) }
            assertFailsWith<IllegalArgumentException>(scheme) { base64.encode(bytes, startIndex = 3, endIndex = 0) }

            assertEquals(symbols.substring(0, 4), base64.encode(bytes, endIndex = 3))
            assertEquals(symbols.substring(4), base64.encode(bytes, startIndex = 3))

            val destination = StringBuilder()
            base64.encodeToAppendable(bytes, destination, endIndex = 3)
            assertEquals(symbols.substring(0, 4), destination.toString())
            base64.encodeToAppendable(bytes, destination, startIndex = 3)
            assertEquals(symbols, destination.toString())
        }

        // encodeToByteArray
        for ((base64, scheme) in codecs) {
            assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.encodeToByteArray(bytes, startIndex = -1) }
            assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.encodeToByteArray(bytes, endIndex = bytes.size + 1) }
            assertFailsWith<IllegalArgumentException>(scheme) { base64.encodeToByteArray(bytes, startIndex = bytes.size + 1) }
            assertFailsWith<IllegalArgumentException>(scheme) { base64.encodeToByteArray(bytes, startIndex = 3, endIndex = 0) }

            assertContentEquals(symbols.encodeToByteArray(0, 4), base64.encodeToByteArray(bytes, endIndex = 3))
            assertContentEquals(symbols.encodeToByteArray(4), base64.encodeToByteArray(bytes, startIndex = 3))

            val destination = ByteArray(8)
            assertFailsWith<IndexOutOfBoundsException> { base64.encodeIntoByteArray(bytes, destination, destinationOffset = -1) }
            assertFailsWith<IndexOutOfBoundsException> { base64.encodeIntoByteArray(bytes, destination, destinationOffset = destination.size + 1) }
            assertFailsWith<IndexOutOfBoundsException> { base64.encodeIntoByteArray(bytes, destination, destinationOffset = 1) }

            assertTrue(destination.all { it == 0.toByte() })

            var length = base64.encodeIntoByteArray(bytes, destination, endIndex = 3)
            assertContentEquals(symbols.encodeToByteArray(0, 4), destination.copyOf(length))
            length += base64.encodeIntoByteArray(bytes, destination, destinationOffset = length, startIndex = 3)
            assertContentEquals(symbols.encodeToByteArray(), destination)
        }

        // decode(CharSequence)
        for ((base64, scheme) in codecs) {
            testDecode(base64, symbols, bytes)
            assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.decode(symbols, startIndex = -1) }
            assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.decode(symbols, endIndex = symbols.length + 1) }
            assertFailsWith<IllegalArgumentException>(scheme) { base64.decode(symbols, startIndex = symbols.length + 1) }
            assertFailsWith<IllegalArgumentException>(scheme) { base64.decode(symbols, startIndex = 4, endIndex = 0) }

            assertContentEquals(bytes.copyOfRange(0, 3), base64.decode(symbols, endIndex = 4))
            assertContentEquals(bytes.copyOfRange(3, bytes.size), base64.decode(symbols, startIndex = 4))

            val destination = ByteArray(6)
            assertFailsWith<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbols, destination, destinationOffset = -1) }
            assertFailsWith<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbols, destination, destinationOffset = destination.size + 1) }
            assertFailsWith<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbols, destination, destinationOffset = 1) }

            assertTrue(destination.all { it == 0.toByte() })

            var length = base64.decodeIntoByteArray(symbols, destination, endIndex = 4)
            assertContentEquals(bytes.copyOfRange(0, 3), destination.copyOf(length))
            length += base64.decodeIntoByteArray(symbols, destination, destinationOffset = length, startIndex = 4)
            assertContentEquals(bytes, destination)
        }

        // decode(ByteArray)
        val symbolBytes = symbols.encodeToByteArray()
        for ((base64, scheme) in codecs) {
            assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.decode(symbolBytes, startIndex = -1) }
            assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.decode(symbolBytes, endIndex = symbolBytes.size + 1) }
            assertFailsWith<IllegalArgumentException>(scheme) { base64.decode(symbolBytes, startIndex = symbolBytes.size + 1) }
            assertFailsWith<IllegalArgumentException>(scheme) { base64.decode(symbolBytes, startIndex = 4, endIndex = 0) }

            assertContentEquals(bytes.copyOfRange(0, 3), base64.decode(symbolBytes, endIndex = 4))
            assertContentEquals(bytes.copyOfRange(3, bytes.size), base64.decode(symbolBytes, startIndex = 4))

            val destination = ByteArray(6)
            assertFailsWith<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = -1) }
            assertFailsWith<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = destination.size + 1) }
            assertFailsWith<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = 1) }

            assertTrue(destination.all { it == 0.toByte() })

            var length = base64.decodeIntoByteArray(symbolBytes, destination, endIndex = 4)
            assertContentEquals(bytes.copyOfRange(0, 3), destination.copyOf(length))
            length += base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = length, startIndex = 4)
            assertContentEquals(bytes, destination)
        }
    }

    @Test
    fun common() {
        fun testEncode(codec: Base64, bytes: String, symbols: String) {
            if (codec.paddingOption.isPresentOnEncode()) {
                testEncode(codec, bytes.encodeToByteArray(), symbols)
            } else {
                testEncode(codec, bytes.encodeToByteArray(), symbols.trimEnd('='))
            }
        }

        fun testDecode(codec: Base64, symbols: String, bytes: String) {
            if (codec.paddingOption.isAllowedOnDecode()) {
                testDecode(codec, symbols, bytes.encodeToByteArray())
                if (codec.paddingOption.isOptionalOnDecode()) {
                    testDecode(codec, symbols.trimEnd('='), bytes.encodeToByteArray())
                }
            } else {
                testDecode(codec, symbols.trimEnd('='), bytes.encodeToByteArray())
            }
        }

        fun testCoding(codec: Base64, bytes: String, symbols: String) {
            testEncode(codec, bytes, symbols)
            testDecode(codec, symbols, bytes)
        }

        for ((codec, scheme) in codecs) {
            // By default, padding option is set to PRESENT
            assertSame(codec, codec.withPadding(Base64.PaddingOption.PRESENT))

            for (paddingOption in Base64.PaddingOption.entries) {
                val configuredCodec = codec.withPadding(paddingOption)
                testCoding(configuredCodec, "", "")
                testCoding(configuredCodec, "f", "Zg==")
                testCoding(configuredCodec, "fo", "Zm8=")
                testCoding(configuredCodec, "foo", "Zm9v")
                testCoding(configuredCodec, "foob", "Zm9vYg==")
                testCoding(configuredCodec, "fooba", "Zm9vYmE=")
                testCoding(configuredCodec, "foobar", "Zm9vYmFy")

                val configuredScheme = "$scheme.withPadding(${paddingOption.name})"

                // at least two symbols are required
                val oneSymbol = listOf("Z", "=", "@")
                for (symbol in oneSymbol) {
                    assertFailsWith<IllegalArgumentException>("$configuredScheme <$symbol>") {
                        configuredCodec.decode(symbol)
                    }.also { exception ->
                        assertContains(exception.message!!, "Input should have at least 2 symbols for Base64 decoding")
                    }
                }

                // dangling single symbol at the end that does not have bits even for a byte
                val lastDanglingSymbol = listOf("Z=", "Z==", "Z===", "Zm9vZ", "Zm9vZ=", "Zm9vZ==", "Zm9vZ===")
                for (symbols in lastDanglingSymbol) {
                    assertFailsWith<IllegalArgumentException>("$configuredScheme <$symbols>") {
                        configuredCodec.decode(symbols)
                    }.also { exception ->
                        assertEquals("The last unit of input does not have enough bits", exception.message)
                    }
                }

                // incorrect padding

                val redundantPadChar = listOf("Zm9v=", "Zm9vYmFy=")
                for (symbols in redundantPadChar) {
                    assertFailsWith<IllegalArgumentException>("$configuredScheme <$symbols>") {
                        configuredCodec.decode(symbols)
                    }.also { exception ->
                        assertEquals("Redundant pad character at index ${symbols.lastIndex}", exception.message)
                    }
                }

                val missingOnePadChar = listOf("Zg=", "Zg=a", "Zm9vYg=", "Zm9vYg=\u0000")
                for (symbols in missingOnePadChar) {
                    val errorMessage = if (paddingOption == Base64.PaddingOption.ABSENT)
                        "The padding option is set to ABSENT"
                    else
                        "Missing one pad character"

                    assertFailsWith<IllegalArgumentException>("$configuredScheme <$symbols>") {
                        configuredCodec.decode(symbols)
                    }.also { exception ->
                        assertContains(exception.message!!, errorMessage)
                    }
                }

                if (paddingOption == Base64.PaddingOption.ABSENT) {
                    val withPadding = listOf("Zg==", "Zm8=", "Zm9vYg==", "Zm9vYmE=")
                    for (symbols in withPadding) {
                        assertFailsWith<IllegalArgumentException>("$configuredScheme <$symbols>") {
                            configuredCodec.decode(symbols)
                        }.also { exception ->
                            assertContains(exception.message!!, "The padding option is set to ABSENT")
                        }
                    }
                }

                if (paddingOption == Base64.PaddingOption.PRESENT) {
                    val withoutPadding = listOf("Zg", "Zm8", "Zm9vYg", "Zm9vYmE")
                    for (symbols in withoutPadding) {
                        assertFailsWith<IllegalArgumentException>("$configuredScheme <$symbols>") {
                            configuredCodec.decode(symbols)
                        }.also { exception ->
                            assertEquals(
                                "The padding option is set to PRESENT, but the input is not properly padded",
                                exception.message
                            )
                        }
                    }
                }

                if (paddingOption.isAllowedOnDecode()) {
                    val symbolAfterPadding = listOf("Zg==Zg==", "Zg===", "Zm9vYmE==", "Zm9vYmE=a")
                    for (symbols in symbolAfterPadding) {
                        assertFailsWith<IllegalArgumentException>("$configuredScheme <$symbols>") {
                            configuredCodec.decode(symbols)
                        }.also { exception ->
                            assertContains(exception.message!!, "prohibited after the pad character")
                        }
                    }
                }

                // the LSBs at positions 1, 2, 3, 4 are not zero, respectively
                val nonZeroPadBits = listOf("Zm9=", "Zm9vYmG=", "Zk==", "Zm9vYo==")
                for (symbols in nonZeroPadBits) {
                    assertFailsWith<IllegalArgumentException>(("$configuredScheme <$symbols>")) {
                        codec.decode(symbols)
                    }.also { exception ->
                        assertEquals("The pad bits must be zeros", exception.message)
                    }
                }
            }
        }
    }

    private val basicAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    private val urlSafeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
    private val alphabetBytes = ByteArray(48) {
        val symbol = it / 3 * 4
        when (it % 3) {
            0 -> (symbol shl 2) + ((symbol + 1) shr 4)
            1 -> ((symbol + 1) and 0xF shl 4) + ((symbol + 2) shr 2)
            else -> ((symbol + 2) and 0x3 shl 6) + (symbol + 3)
        }.toByte()
    }

    @Test
    fun basic() {
        testCoding(Base64, bytes(0b1111_1011, 0b1111_0000), "+/A=")

        // all symbols from alphabet
        testCoding(Base64, alphabetBytes, basicAlphabet)

        // decode line separator
        assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9v\r\nYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9v\nYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9\rvYg==") }

        // decode illegal char
        assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9vY(==") }
        assertFailsWith<IllegalArgumentException> { Base64.decode("Zm[@]9vYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9v-Yg==") }
        assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9vYg=(%^)=") }
        assertFailsWith<IllegalArgumentException> { Base64.decode("Zm\u00FF9vYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.decode("\uFFFFZm9vYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9vYg==\uD800\uDC00") }

        // no line separator inserted
        val expected = "Zm9vYmFy".repeat(76)
        testEncode(Base64, "foobar".repeat(76).encodeToByteArray(), expected)
    }

    @Test
    fun urlSafe() {
        testCoding(Base64.UrlSafe, bytes(0b1111_1011, 0b1111_0000), "-_A=")

        // all symbols from alphabet
        testCoding(Base64.UrlSafe, alphabetBytes, urlSafeAlphabet)

        // decode line separator
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9v\r\nYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9v\nYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9\rvYg==") }

        // decode illegal char
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9vY(==") }
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm[@]9vYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9v+Yg==") }
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9vYg=(%^)=") }
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm\u00FF9vYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("\uFFFFZm9vYg==") }
        assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9vYg==\uD800\uDC00") }

        // no line separator inserted
        val expected = "Zm9vYmFy".repeat(76)
        testEncode(Base64.UrlSafe, "foobar".repeat(76).encodeToByteArray(), expected)
    }

    @Test
    fun mime() {
        for ((codec, lineLength) in listOf(Base64.Mime to 76, Base64.Pem to 64)) {
            testCoding(codec, bytes(0b1111_1011, 0b1111_0000), "+/A=")

            // all symbols from alphabet
            testCoding(codec, alphabetBytes, basicAlphabet)

            // dangling single symbol
            assertFailsWith<IllegalArgumentException> { codec.decode("Zm9vY(==") }

            // decode line separator
            testDecode(codec, "Zm9v\r\nYg==", "foob".encodeToByteArray())
            testDecode(codec, "Zm9v\nYg==", "foob".encodeToByteArray())
            testDecode(codec, "Zm9\rvYg==", "foob".encodeToByteArray())

            // decode illegal char
            testDecode(codec, "Zm9vYg(==", "foob".encodeToByteArray())
            testDecode(codec, "Zm[@]9vYg==", "foob".encodeToByteArray())
            testDecode(codec, "Zm9v-Yg==", "foob".encodeToByteArray())
            testDecode(codec, "Zm9vYg=(%^)=", "foob".encodeToByteArray())
            testDecode(codec, "Zm\u00FF9vYg==", "foob".encodeToByteArray())
            testDecode(codec, "\uFFFFZm9vYg==", "foob".encodeToByteArray())
            testDecode(codec, "Zm9vYg==\uD800\uDC00", "foob".encodeToByteArray())

            // inserts line separator, but not to the end of the output
            val expected = "Zm9vYmFy".repeat(100).chunked(lineLength).joinToString(separator = "\r\n")
            testEncode(codec, "foobar".repeat(100).encodeToByteArray(), expected)
        }
    }

    @Test
    fun encodeSize() {
        for ((codec, _) in codecs) {
            val lineSeparatorChars = if (codec.isMimeScheme) 2 else 0
            val mimeSeparatedLineLength = codec.mimeLineLength + lineSeparatorChars
            val oneLineInputBytes = codec.mimeLineLength / Base64.symbolsPerGroup * Base64.bytesPerGroup

            val paddingPresent = Base64.PaddingOption.entries.filter { it.isPresentOnEncode() }
            for (paddingOption in paddingPresent) {
                val configuredCodec = codec.withPadding(paddingOption)

                // One line in all schemes

                assertEquals(0, configuredCodec.encodeSize(0))
                assertEquals(4, configuredCodec.encodeSize(1))
                assertEquals(4, configuredCodec.encodeSize(2))
                assertEquals(4, configuredCodec.encodeSize(3))
                assertEquals(8, configuredCodec.encodeSize(4))
                assertEquals(8, configuredCodec.encodeSize(5))
                assertEquals(8, configuredCodec.encodeSize(6))
                assertEquals(12, configuredCodec.encodeSize(7))
                assertEquals(12, configuredCodec.encodeSize(8))
                assertEquals(12, configuredCodec.encodeSize(9))

                if (codec.isMimeScheme) {
                    // Two lines in mime scheme
                    assertEquals(codec.mimeLineLength, configuredCodec.encodeSize(oneLineInputBytes))
                    assertEquals(codec.mimeLineLength + lineSeparatorChars + Base64.symbolsPerGroup, configuredCodec.encodeSize(oneLineInputBytes + 1)) // line separator
                    assertEquals(codec.mimeLineLength + lineSeparatorChars + Base64.symbolsPerGroup, configuredCodec.encodeSize(oneLineInputBytes + 2))

                    // Three lines in mime scheme
                    assertEquals(2 * codec.mimeLineLength + lineSeparatorChars, configuredCodec.encodeSize(2 * oneLineInputBytes))
                    assertEquals(2 * codec.mimeLineLength + 2 * lineSeparatorChars + Base64.symbolsPerGroup, configuredCodec.encodeSize(2 * oneLineInputBytes + 1)) // line separator
                    assertEquals(2 * codec.mimeLineLength + 2 * lineSeparatorChars + Base64.symbolsPerGroup, configuredCodec.encodeSize(2 * oneLineInputBytes + 2))
                } else {
                    assertEquals(76, configuredCodec.encodeSize(57))
                    assertEquals(80, configuredCodec.encodeSize(58)) // no line separator
                    assertEquals(80, configuredCodec.encodeSize(59))
                    assertEquals(152, configuredCodec.encodeSize(114))
                    assertEquals(156, configuredCodec.encodeSize(115)) // no line separator
                    assertEquals(156, configuredCodec.encodeSize(116))
                }

                // The maximum number of bytes that we can encode

                if (codec.isMimeScheme) {
                    val fullLines = Int.MAX_VALUE / mimeSeparatedLineLength
                    val fullLinesChars = fullLines * mimeSeparatedLineLength
                    val fullLinesBytes = fullLines * oneLineInputBytes
                    val lastLineChars = Int.MAX_VALUE % mimeSeparatedLineLength
                    val lastLineBytes = lastLineChars / Base64.symbolsPerGroup * Base64.bytesPerGroup

                    for (lastLine in 1..lastLineBytes) {
                        assertEquals(fullLinesChars + configuredCodec.encodeSize(lastLine), configuredCodec.encodeSize(fullLinesBytes + lastLine))
                    }
                    assertFailsWith<IllegalArgumentException> {
                        configuredCodec.encodeSize(fullLinesBytes + lastLineBytes + 1) // Int.MAX_VALUE + 3
                    }.also { exception ->
                        assertEquals("Input is too big", exception.message)
                    }
                } else {
                    val limit = 1_610_612_733
                    assertEquals(2_147_483_644, configuredCodec.encodeSize(limit))
                    assertFailsWith<IllegalArgumentException> {
                        configuredCodec.encodeSize(limit + 1) // Int.MAX_VALUE + 1
                    }.also { exception ->
                        assertEquals("Input is too big", exception.message)
                    }
                }

                assertFailsWith<IllegalArgumentException> {
                    configuredCodec.encodeSize(Int.MAX_VALUE)
                }.also { exception ->
                    assertEquals("Input is too big", exception.message)
                }
            }

            val paddingAbsent = Base64.PaddingOption.entries - paddingPresent.toSet()
            for (paddingOption in paddingAbsent) {
                val configuredCodec = codec.withPadding(paddingOption)

                // One line in all schemes

                assertEquals(0, configuredCodec.encodeSize(0))
                assertEquals(2, configuredCodec.encodeSize(1))
                assertEquals(3, configuredCodec.encodeSize(2))
                assertEquals(4, configuredCodec.encodeSize(3))
                assertEquals(6, configuredCodec.encodeSize(4))
                assertEquals(7, configuredCodec.encodeSize(5))
                assertEquals(8, configuredCodec.encodeSize(6))
                assertEquals(10, configuredCodec.encodeSize(7))
                assertEquals(11, configuredCodec.encodeSize(8))
                assertEquals(12, configuredCodec.encodeSize(9))

                if (codec.isMimeScheme) {
                    // Two lines in mime scheme
                    assertEquals(codec.mimeLineLength, configuredCodec.encodeSize(oneLineInputBytes))
                    assertEquals(codec.mimeLineLength + lineSeparatorChars + 2, configuredCodec.encodeSize(oneLineInputBytes + 1)) // line separator
                    assertEquals(codec.mimeLineLength + lineSeparatorChars + 3, configuredCodec.encodeSize(oneLineInputBytes + 2))

                    // Three lines in mime scheme
                    assertEquals(2 * codec.mimeLineLength + lineSeparatorChars, configuredCodec.encodeSize(2 * oneLineInputBytes))
                    assertEquals(2 * codec.mimeLineLength + 2 * lineSeparatorChars + 2, configuredCodec.encodeSize(2 * oneLineInputBytes + 1)) // line separator
                    assertEquals(2 * codec.mimeLineLength + 2 * lineSeparatorChars + 3, configuredCodec.encodeSize(2 * oneLineInputBytes + 2))
                } else {
                    assertEquals(76, configuredCodec.encodeSize(57))
                    assertEquals(78, configuredCodec.encodeSize(58)) // no line separator
                    assertEquals(79, configuredCodec.encodeSize(59))
                    assertEquals(152, configuredCodec.encodeSize(114))
                    assertEquals(154, configuredCodec.encodeSize(115)) // no line separator
                    assertEquals(155, configuredCodec.encodeSize(116))
                }

                // The maximum number of bytes that we can encode

                if (codec.isMimeScheme) {
                    val fullLines = Int.MAX_VALUE / mimeSeparatedLineLength
                    val fullLinesChars = fullLines * mimeSeparatedLineLength
                    val fullLinesBytes = fullLines * oneLineInputBytes
                    val lastLineChars = Int.MAX_VALUE % mimeSeparatedLineLength
                    val lastLineBytes = lastLineChars / Base64.symbolsPerGroup * Base64.bytesPerGroup

                    for (lastLine in 1..lastLineBytes) {
                        assertEquals(fullLinesChars + configuredCodec.encodeSize(lastLine), configuredCodec.encodeSize(fullLinesBytes + lastLine))
                    }
                    assertFailsWith<IllegalArgumentException> {
                        configuredCodec.encodeSize(fullLinesBytes + lastLineBytes + 1) // Int.MAX_VALUE + 1
                    }.also { exception ->
                        assertEquals("Input is too big", exception.message)
                    }
                } else {
                    val limit = 1_610_612_733
                    assertEquals(2_147_483_644, configuredCodec.encodeSize(limit))
                    assertEquals(2_147_483_646, configuredCodec.encodeSize(limit + 1))
                    assertEquals(2_147_483_647, configuredCodec.encodeSize(limit + 2)) // Int.MAX_VALUE
                    assertFailsWith<IllegalArgumentException> {
                        configuredCodec.encodeSize(limit + 3) // Int.MAX_VALUE + 1
                    }.also { exception ->
                        assertEquals("Input is too big", exception.message)
                    }
                }

                assertFailsWith<IllegalArgumentException> {
                    configuredCodec.encodeSize(Int.MAX_VALUE)
                }.also { exception ->
                    assertEquals("Input is too big", exception.message)
                }
            }
        }
    }

    @Test
    fun decodeSize() {
        fun testDecodeSize(codec: Base64, symbols: String, expectedSize: Int) {
            assertEquals(expectedSize, codec.decodeSize(symbols.encodeToByteArray(), 0, symbols.length))
            assertEquals(
                expectedSize,
                codec.decodeSize(
                    if (symbols.isEmpty())
                        ByteArray(10)
                    else
                        ByteArray(symbols.length + 10) { symbols[(it - 5).coerceIn(0, symbols.lastIndex)].code.toByte() },
                    startIndex = 5,
                    endIndex = symbols.length + 5
                )
            )
        }

        for ((codec, _) in codecs) {
            assertFailsWith<IllegalArgumentException> {
                codec.decode(ByteArray(1), 0, 1)
            }.also { exception ->
                assertEquals("Input should have at least 2 symbols for Base64 decoding, startIndex: 0, endIndex: 1", exception.message)
            }
            assertFailsWith<IllegalArgumentException> {
                codec.decode(ByteArray(11), 5, 6)
            }.also { exception ->
                assertEquals("Input should have at least 2 symbols for Base64 decoding, startIndex: 5, endIndex: 6", exception.message)
            }

            testDecodeSize(codec, "", 0)
            testDecodeSize(codec, "Zg==", 1)
            testDecodeSize(codec, "Zg=", 1)
            testDecodeSize(codec, "Zg", 1)
            testDecodeSize(codec, "Zm8=", 2)
            testDecodeSize(codec, "Zm8", 2)
            testDecodeSize(codec, "Zm9v", 3)
            testDecodeSize(codec, "Zm9vYg==", 4)
            testDecodeSize(codec, "Zm9vYg=", 4)
            testDecodeSize(codec, "Zm9vYg", 4)
            testDecodeSize(codec, "Zm9vYmE=", 5)
            testDecodeSize(codec, "Zm9vYmE", 5)
            testDecodeSize(codec, "Zm9vYmFy", 6)

            val longSymbols = "Zm9vYmFy".repeat(76)
            testDecodeSize(codec, longSymbols, 6 * 76)
            testDecodeSize(codec, longSymbols + "Zg==", 6 * 76 + 1)
            testDecodeSize(codec, longSymbols + "Zg=", 6 * 76 + 1)
            testDecodeSize(codec, longSymbols + "Zg", 6 * 76 + 1)

            if (codec.isMimeScheme) {
                testDecodeSize(codec, "Zg==" + longSymbols, 1)
                testDecodeSize(codec, "Zg=" + longSymbols, 1)
                testDecodeSize(codec, "Zg" + longSymbols, 1 + 6 * 76)
            } else {
                testDecodeSize(codec, "Zg==" + longSymbols, 3 + 6 * 76)
                testDecodeSize(codec, "Zg=" + longSymbols, 2 + 6 * 76)
                testDecodeSize(codec, "Zg" + longSymbols, 1 + 6 * 76)
            }
        }
    }
}