/*
 * This file is part of LibEuFin.
 * Copyright (C) 2023-2025 Taler Systems S.A.
 *
 * LibEuFin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3, or
 * (at your option) any later version.
 *
 * LibEuFin is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with LibEuFin; see the file COPYING.  If not, see
 * <http://www.gnu.org/licenses/>
 */

import com.github.ajalt.clikt.testing.test
import org.junit.Test
import tech.libeufin.common.*
import tech.libeufin.common.db.currentUser
import tech.libeufin.common.db.jdbcFromPg
import uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariable
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import java.time.Duration
import kotlin.io.path.*
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFailsWith

class ConfigTest {
    @Test
    fun cli() {
        val cmd = CliConfigCmd(ConfigSource("test", "test", "test"))
        val configPath = Path("tmp/test-conf.conf")
        val secondPath = Path("tmp/test-second-conf.conf")

        fun testErr(msg: String) {
            val prevErr = System.err
            val tmpErr = ByteArrayOutputStream()
            System.setErr(PrintStream(tmpErr))
            val result = cmd.test("dump -c $configPath")
            System.setErr(prevErr)
            val lastLog = tmpErr.asUtf8().substringAfterLast(" - ").trimEnd('\n')
            assertEquals(1, result.statusCode, lastLog)
            assertEquals(msg, lastLog, lastLog)
        }

        configPath.deleteIfExists()
        testErr("Could not read config at '$configPath': no such file")

        configPath.createParentDirectories()
        configPath.createFile()
        configPath.toFile().setReadable(false)
        if (!configPath.isReadable()) { // Skip if root
            testErr("Could not read config at '$configPath': permission denied")
        }

        configPath.toFile().setReadable(true)
        configPath.writeText("@inline@test-second-conf.conf")
        secondPath.deleteIfExists()
        testErr("Could not read config at '$secondPath': no such file")

        secondPath.createFile()
        secondPath.toFile().setReadable(false)
        if (!secondPath.isReadable()) { // Skip if root
            testErr("Could not read config at '$secondPath': permission denied")
        }
        
        configPath.writeText("@inline-matching@[*")
        testErr("Malformed glob regex at '$configPath:0': Missing '] near index 1\n[*\n ^")

        configPath.writeText("@inline-matching@*second-conf.conf")
        if (!secondPath.isReadable()) { // Skip if root
            testErr("Could not read config at '$secondPath': permission denied")
        }

        secondPath.toFile().setReadable(true)
        configPath.writeText("\n@inline-matching@*.conf")
        testErr("Recursion limit in config inlining at '$secondPath:1'")
        configPath.writeText("\n\n@inline@test-conf.conf")
        testErr("Recursion limit in config inlining at '$configPath:2'")
    }
    
    fun checkErr(msg: String, block: () -> Unit) {
        val exception = assertFailsWith<TalerConfigError>(null, block)
        println(exception.message)
        assertEquals(msg, exception.message)
    }

    @Test
    fun parsing() {
        checkErr("expected section header at 'mem:1'") {
            ConfigSource("test", "test", "test").fromMem(
                """
                key=value
                """
            )
        }

        checkErr("expected section header, option assignment or directive at 'mem:2'") {
            ConfigSource("test", "test", "test").fromMem(
                """
                [section]
                bad-line
                """
            )
        }

        ConfigSource("test", "test", "test").fromMem(
            """

            [section-a]

            bar = baz

            [section-b]

            first_value = 1
            second_value = "test"

            """.trimIndent()
        ).let { conf ->
            // Missing section
            checkErr("Missing string option 'value' in section 'unknown'") {
                conf.section("unknown").string("value").require()
            }

            // Missing value
            checkErr("Missing string option 'value' in section 'section-a'") {
                conf.section("section-a").string("value").require()
            }
        }
    }

    fun <T> testConfigValue(
        type: String,
        lambda: TalerConfigSection.(String) -> TalerConfigOption<T>,
        wellformed: List<Pair<List<String>, T>>,
        malformed: List<Pair<List<String>, (String) -> String>>,
        conf: String = ""
    ) {
        fun conf(content: String) = ConfigSource("test", "test", "test").fromMem("$conf\n$content")

        // Check missing msg
        val conf = conf("")
        checkErr("Missing $type option 'value' in section 'section'") {
            conf.section("section").lambda("value").require()
        }

        // Check wellformed options are properly parsed
        for ((raws, expected) in wellformed) {
            for (raw in raws) {
                val conf = conf("[section]\nvalue=$raw")
                assertEquals(expected, conf.section("section").lambda("value").require())
            }
        }

        // Check malformed options have proper error message
        for ((raws, errorFmt) in malformed) {
            for (raw in raws) {
                val conf = conf("[section]\nvalue=$raw")
                checkErr("Expected $type option 'value' in section 'section': ${errorFmt(raw)}") {
                    conf.section("section").lambda("value").require()
                }
            }
        }
    }
    fun <T> testConfigValue(
        type: String,
        lambda: TalerConfigSection.(String) -> TalerConfigOption<T>,
        wellformed: List<Pair<List<String>, T>>,
        malformed: Pair<List<String>, (String) -> String>
    ) = testConfigValue(type, lambda, wellformed, listOf(malformed))

    @Test
    fun string() = testConfigValue(
        "string", TalerConfigSection::string, listOf(
            listOf("1", "\"1\"") to "1",
            listOf("test", "\"test\"") to "test",
            listOf("\"") to "\"",
        ), listOf()
    )

    @Test
    fun number() = testConfigValue(
        "number", TalerConfigSection::number, listOf(
            listOf("1") to 1,
            listOf("42") to 42
        ), listOf("true", "YES") to { "'$it' not a valid number" }
    )

    @Test
    fun boolean() = testConfigValue(
        "boolean", TalerConfigSection::boolean, listOf(
            listOf("yes", "YES", "Yes") to true,
            listOf("no", "NO", "No") to false
        ), listOf("true", "1") to { "expected 'YES' or 'NO' got '$it'" }
    )

    @Test
    fun path() = testConfigValue(
        "path", TalerConfigSection::path,
        listOf(
            listOf("path") to Path("path"),
            listOf("foo/\$DATADIR/bar", "foo/\${DATADIR}/bar") to Path("foo/mydir/bar"),
            listOf("foo/\$DATADIR\$DATADIR/bar") to Path("foo/mydirmydir/bar"),
            listOf("foo/pre_\$DATADIR/bar", "foo/pre_\${DATADIR}/bar") to Path("foo/pre_mydir/bar"),
            listOf("foo/\${DATADIR}_next/bar", "foo/\${UNKNOWN:-\$DATADIR}_next/bar") to Path("foo/mydir_next/bar"),
            listOf("foo/\${UNKNOWN:-default}_next/bar", "foo/\${UNKNOWN:-\${UNKNOWN:-default}}_next/bar") to Path("foo/default_next/bar"),
            listOf("foo/\${UNKNOWN:-pre_\${UNKNOWN:-default}_next}_next/bar") to Path("foo/pre_default_next_next/bar"),
        ),
        listOf(
            listOf("foo/\${A/bar") to { "bad substitution '\${A/bar'" },
            listOf("foo/\${A:-pre_\${B}/bar") to { "unbalanced variable expression 'pre_\${B}/bar'" },
            listOf("foo/\${A:-\${B\${C}/bar") to { "unbalanced variable expression '\${B\${C}/bar'" },
            listOf("foo/\$UNKNOWN/bar", "foo/\${UNKNOWN}/bar") to { "unbound variable 'UNKNOWN'" },
            listOf("foo/\$RECURSIVE/bar") to { "recursion limit in path substitution exceeded for '\$RECURSIVE'" }
        ),
        "[PATHS]\nDATADIR=mydir\nRECURSIVE=\$RECURSIVE"
    )

    @Test
    fun duration() = testConfigValue(
        "temporal", TalerConfigSection::duration,
        listOf(
            listOf("1s", "1 s") to Duration.ofSeconds(1),
            listOf("10m", "10 m") to Duration.ofMinutes(10),
            listOf("1h") to Duration.ofHours(1),
            listOf("1h10m12s", "1h 10m 12s", "1 h 10 m 12 s", "1h10'12\"") to
                Duration.ofHours(1).plus(Duration.ofMinutes(10)).plus(Duration.ofSeconds(12)),
        ),
        listOf(
            listOf("test", "42") to { "'$it' not a valid temporal" },
            listOf("42t") to { "'t' not a valid temporal unit" },
            listOf("9223372036854775808s") to { "'9223372036854775808' not a valid temporal amount" },
        )
    )

    @Test
    fun date() = testConfigValue(
        "date", TalerConfigSection::date,
        listOf(
            listOf("2024-12-12") to dateToInstant("2024-12-12"),
        ),
        listOf(
            listOf("test", "42") to { "'$it' not a valid date" },
            listOf("2024-12-32") to { "'$it' not a valid date: Invalid value for DayOfMonth (valid values 1 - 28/31): 32" },
            listOf("2024-42-12") to { "'$it' not a valid date: Invalid value for MonthOfYear (valid values 1 - 12): 42" },
            listOf("2024-12-32s") to { "'$it' not a valid date at index 10" },
        )
    )

    @Test
    fun jsonMap() = testConfigValue(
        "json key/value map", TalerConfigSection::jsonMap,
        listOf(
            listOf("{\"a\": \"12\", \"b\": \"test\"}") to mapOf("a" to "12", "b" to "test"),
        ),
        listOf("test", "12", "{\"a\": 12}", "{\"a\": \"12\",") to { "'$it' is malformed" }
    )

    @Test
    fun amount() = testConfigValue(
        "amount", { amount(it, "KUDOS") },
        listOf(
            listOf("KUDOS:12", "KUDOS:12.0", "KUDOS:012.0") to TalerAmount("KUDOS:12"),
        ),
        listOf(
            listOf("test", "42", "KUDOS:0.3ABC") to { "'$it' is malformed: Invalid amount format" },
            listOf("KUDOS:999999999999999999") to { "'$it' is malformed: Value specified in amount is too large" },
            listOf("EUR:12") to { "expected currency KUDOS got EUR" },
        )
    )

    @Test
    fun map() = testConfigValue(
        "map", { map(it, "map", mapOf("one" to 1, "two" to 2, "three" to 3)) },
        listOf(
            listOf("one") to 1,
            listOf("two") to 2,
            listOf("three") to 3,
        ),
        listOf(
            listOf("test", "42") to { "expected 'one', 'two' or 'three' got '$it'" },
        )
    )

    @Test
    fun mapLambda() = testConfigValue(
        "lambda",
        { 
            map(it, "lambda", mapOf("ok" to 1, "fail" to { throw Exception("Never executed") }))
        },
        listOf(
            listOf("ok") to 1,
        ),
        listOf(
            listOf("test", "42") to { "expected 'ok' or 'fail' got '$it'" }
        )
    )

    @Test
    fun jdbcParsing() {
        val user = currentUser()
        assertFails { jdbcFromPg("test") }
        assertEquals("jdbc:test", jdbcFromPg("jdbc:test"))
        assertEquals("jdbc:postgresql://localhost/?user=$user&socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory\$FactoryArg&socketFactoryArg=/var/run/postgresql/.s.PGSQL.5432", jdbcFromPg("postgresql:///"))
        assertEquals("jdbc:postgresql://?host=args%2Dhost&user=arg%23%24User&password=%21%22%23%24%25%26%27%28%29", jdbcFromPg("postgresql://?host=args%2Dhost&user=arg%23%24User&password=%21%22%23%24%25%26%27%28%29"))
        withEnvironmentVariable("PGPORT", "1234").execute {
            assertEquals("jdbc:postgresql://localhost/?user=$user&socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory\$FactoryArg&socketFactoryArg=/var/run/postgresql/.s.PGSQL.1234", jdbcFromPg("postgresql:///"))
        }
        withEnvironmentVariable("PGPORT", "1234").and("PGHOST", "/tmp").execute {
            assertEquals("jdbc:postgresql://localhost/?user=$user&socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory\$FactoryArg&socketFactoryArg=/tmp/.s.PGSQL.1234", jdbcFromPg("postgresql:///"))
        }
    }
}
