package ch.dissem.yaep.domain

import ch.dissem.yaep.domain.Animal.*
import ch.dissem.yaep.domain.Nationality.*
import ch.dissem.yaep.domain.Profession.*
import ch.dissem.yaep.domain.PuzzleSolution.SOLVABLE
import ch.tutteli.atrium.api.fluent.en_GB.feature
import ch.tutteli.atrium.api.fluent.en_GB.toBeGreaterThan
import ch.tutteli.atrium.api.fluent.en_GB.toBeLessThan
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
import ch.tutteli.atrium.api.verbs.expect
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.measureTime

class GameTest {
    private val log = KotlinLogging.logger {}

    @Test
    fun `ensure generated games are solvable`() {
        val tries = 100
        var fastest = 500.milliseconds
        var slowest = 0.milliseconds
        var total = 0.milliseconds
        var most = 0
        var least = 1000
        var totalClues = 0
        for (i in 1..tries) {
            val game: Game
            val time = measureTime {
                game = generateGame()
            }
            log.info { "Generated game #$i in ${time.inWholeMilliseconds}ms" }
            val solvable = solve(game.grid, game.clues)
            if (solvable != SOLVABLE) {
                log.info { "Puzzle:\n$game" }
            }
            expect(solvable).toEqual(SOLVABLE)
            expect(time).toBeLessThan(1000.milliseconds)
            if (time < fastest) {
                fastest = time
            }
            if (time > slowest) {
                slowest = time
            }
            total += time
            if (game.clues.size > most) {
                most = game.clues.size
            }
            if (game.clues.size < least) {
                least = game.clues.size
            }
            totalClues += game.clues.size
        }

        log.info { "Slowest: $slowest" }
        log.info { "Fastest: $fastest" }
        log.info { "Average: ${total / tries}" }
        log.info { "Clues:" }
        log.info { "Most: $most" }
        log.info { "Least: $least" }
        log.info { "Average: ${totalClues / tries}" }
    }

    @Test
    fun `ensure generated game is valid`() {
        val game: Game
        val time = measureTime {
            game = generateGame()
        }
        expect(game) {
            feature(Game::areCategoriesValid).toEqual(true)
            feature(Game::isValid).toEqual(true)
            feature(Game::clues) {
                feature(Collection<Clue>::size).toBeGreaterThan(5)
                feature(Collection<Clue>::size).toBeLessThan(30)
            }
        }
        log.info{"Clues: ${game.clues.size}"}
        log.info{"Time: $time"}
        expect(solve(game.grid, game.clues)).toEqual(SOLVABLE)
        expect(time).toBeLessThan(500.milliseconds)
    }

    @Test
    fun `ensure game can be solved`() {
        val professions = listOf(
            Item(ASTRONAUT),
            Item(FARMER),
            Item(SOFTWARE_DEV)
        )
        val animals = listOf(
            Item(SNAIL),
            Item(ANT),
            Item(OCTOPUS)
        )
        val nationalities = listOf(
            Item(UKRAINE),
            Item(CANADA),
            Item(SWITZERLAND)
        )
        val game = Game(
            Grid(listOf(
                GameRow(
                    Profession,
                    professions,
                    professions.map { GameCell(null, it, professions.toMutableList()) }
                ),
                GameRow(
                    Animal,
                    animals,
                    animals.map { GameCell(null, it, animals.toMutableList()) }
                ),
                GameRow(
                    Nationality,
                    nationalities,
                    nationalities.map { GameCell(null, it, nationalities.toMutableList()) }
                )
            )),
            listOf(
                TripletClue(professions[0], animals[1], nationalities[2]),
                OrderClue(professions[0], nationalities[1]),
                SameColumnClue(animals[1], nationalities[1]),
                NeighbourClue(professions[0], professions[1]),
                PositionClue(animals[0], 0)
            )
        )

        expect(solve(game.grid, game.clues)).toEqual(SOLVABLE)
    }

    @Test
    fun `ensure specific game is solvable`() {
        val game = Game.parse(
            """
            * ZEBRA is between the neighbours PIE and PEAR to both sides
            * MATE is at position 0
            * SLOTH is between the neighbours ZEBRA and COFFEE to both sides
            * ICE_CREAM is left of MANGO
            * SWITZERLAND is at position 0
            * PIE is at position 4
            * SCIENTIST is between the neighbours ASTRONAUT and PEAR to both sides
            * ROCK_STAR is between the neighbours SNAIL and ANT to both sides
            * SNAIL is between the neighbours ROCK_STAR and TEA to both sides
            * SOFTWARE_DEV is left of SLOTH
            * SOFTWARE_DEV is left of HEALTH_WORKER
            * MILK is between the neighbours CUSTARD and ZEBRA to both sides
            * SLOTH is between the neighbours CUSTARD and CAKE to both sides
            * SPAIN is between the neighbours CUSTARD and GRAPES to both sides
            * SCIENTIST is between the neighbours SNAIL and SLOTH to both sides
            * DOG is between the neighbours CUPCAKE and BUBBLE_TEA to both sides
            * SNAIL is between the neighbours BANANA and GRAPES to both sides
            * SLOTH is between the neighbours GRAPES and CANADA to both sides
            * UKRAINE and SCIENTIST are in the same column
            * DOG is between the neighbours JAPAN and SWITZERLAND to both sides
            * SLOTH is between the neighbours GOAT and TEA to both sides
            * ROCK_STAR and UNITED_KINGDOM are in the same column
            * ROCK_STAR is next to DOUGHNUT
            * PINEAPPLE is between the neighbours TEACHER and GRAPES to both sides
        """.trimIndent()
        )

        expect(solve(game.grid, game.clues)).toEqual(SOLVABLE)
    }
}
