/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.grails.plugins.databasemigration.liquibase

import grails.core.GrailsApplication
import liquibase.exception.CommandExecutionException
import org.apache.grails.data.hibernate7.dbmigration.cli.ApplicationContextDatabaseMigrationCommandSpec
import org.apache.grails.data.hibernate7.dbmigration.cli.DbmChangelogSyncCommand
import org.apache.grails.data.hibernate7.dbmigration.cli.DbmRollbackCommand
import org.apache.grails.data.hibernate7.dbmigration.cli.DbmTagCommand
import org.apache.grails.data.hibernate7.dbmigration.cli.DbmUpdateCommand
import org.apache.grails.data.hibernate7.dbmigration.cli.DbmUpdateCountCommand

class GroovyChangeLogSpec extends ApplicationContextDatabaseMigrationCommandSpec {

    static List<String> calledBlocks

    def setup() {
        calledBlocks = []
        Locale.setDefault(new Locale("en", "US"))
    }

    def "updates a database with Groovy Change"() {
        given:
        def command = createCommand(DbmUpdateCommand)
        command.changeLogFile << """
databaseChangeLog = {
    changeSet(author: "John Smith", id: "1") {
        grailsChange {
            init { ${GroovyChangeLogSpec.name}.calledBlocks << 'init' }
            validate { ${GroovyChangeLogSpec.name}.calledBlocks << 'validate' }
            change { ${GroovyChangeLogSpec.name}.calledBlocks << 'change' }
            rollback { ${GroovyChangeLogSpec.name}.calledBlocks << 'rollback' }
            confirm 'confirmation message'
            checkSum 'override value for checksum'
        }
    }
}
"""
        when:
        command.handle(getExecutionContext(DbmUpdateCommand))

        then: 'all change-set closures executed in the documented order'
        // Per-changeset Liquibase log lines (e.g. confirmation message) are
        // emitted via Liquibase's LogService whose default implementation
        // depends on classpath state (Slf4jLogService vs JavaLogService) and
        // whose level depends on the active Logback / java.util.logging
        // config. We deliberately do not assert on captured stdout/stderr
        // for those messages because both legs are environment-dependent
        // (e.g. they fail intermittently in the Apache Groovy joint
        // validation build). The change being applied is verified by
        // calledBlocks; the confirmation message is exercised by the
        // changelog parser populating GroovyChange.confirmationMessage.
        calledBlocks == ['init', 'validate', 'change']
    }


    def "outputs a warning message by calling the warn method"() {
        given:
        def command = createCommand(DbmUpdateCommand)
        command.changeLogFile << """
databaseChangeLog = {
    changeSet(author: "John Smith", id: "2") {
        grailsChange {
            validate {
                ${GroovyChangeLogSpec.name}.calledBlocks << 'validate'
                warn('warn message')
            }
            change {
                ${GroovyChangeLogSpec.name}.calledBlocks << 'change'
            }
        }
    }
}
"""
        when:
        command.handle(getExecutionContext(DbmUpdateCommand))

        then: 'validate-with-warn closure runs and the change applies normally'
        // See the explanatory comment on 'updates a database with Groovy
        // Change' above - asserting on stdout for the warn message is
        // unreliable across SLF4J / JUL classpath permutations.
        calledBlocks == ['validate', 'change']
    }

    def "stops processing by calling the error method"() {
        given:
        DbmUpdateCommand command = (DbmUpdateCommand) createCommand(DbmUpdateCommand)
        command.changeLogFile << """
databaseChangeLog = {
    changeSet(author: "John Smith", id: "1") {
        grailsChange {
            validate {
                ${GroovyChangeLogSpec.name}.calledBlocks << 'validate'
                error('error message')
            }
            change {
                ${GroovyChangeLogSpec.name}.calledBlocks << 'change'
            }
        }
    }
}
"""
        when:
        command.handle(getExecutionContext(DbmUpdateCommand))

        then:
        def e = thrown(CommandExecutionException)

        e.message.contains('1 changes have validation failures')
        e.message.contains('error message, changelog.groovy::1::John Smith')
        calledBlocks == ['validate']
    }


    def "can use bind variables in the change block"() {
        given:
        def command = createCommand(DbmUpdateCommand)
        command.changeLogFile << """
databaseChangeLog = {
    changeSet(author: "John Smith", id: "4") {
        grailsChange {
            change {
                assert changeSet.id == '4'
                assert resourceAccessor.toString().startsWith('CompositeResourceAccessor{')
                assert ctx.hashCode() == ${applicationContext.hashCode()}
                assert application.hashCode() == ${applicationContext.getBean(GrailsApplication).hashCode()}
                ${GroovyChangeLogSpec.name}.calledBlocks << 'change'
            }
        }
    }
}
"""
        when:
        command.handle(getExecutionContext(DbmUpdateCommand))

        then:
        calledBlocks == ['change']
    }


    def "executes sql statements in the change block"() {
        given:
        def command = createCommand(DbmUpdateCommand)
        command.changeLogFile << """
import groovy.sql.Sql
import liquibase.statement.core.InsertStatement

databaseChangeLog = {
    changeSet(author: "John Smith", id: "5") {
        grailsChange {
            change {
                new Sql(database.connection.underlyingConnection).executeUpdate('CREATE TABLE book (id INT)')
                new Sql(databaseConnection.underlyingConnection).executeUpdate('INSERT INTO book (id) VALUES (1)')
                new Sql(connection).executeUpdate('INSERT INTO book (id) VALUES (2)')
                sqlStatement(new InsertStatement(null, null, 'book').addColumnValue('id', 3))
                sqlStatements([new InsertStatement(null, null, 'book').addColumnValue('id', 4), new InsertStatement(null, null, 'book').addColumnValue('id', 5)])
            }
        }
    }
}
"""

        when:
        command.handle(getExecutionContext(DbmUpdateCommand))

        then:
        sql.rows('SELECT id FROM book').collect { it.id } as Set == [1, 2, 3, 4, 5] as Set
    }


    def "rolls back a database with Groovy Change"() {
        given:
        def command = createCommand(DbmRollbackCommand)
        command.changeLogFile << """
databaseChangeLog = {
    changeSet(author: "John Smith", id: "6") {
    }
    changeSet(author: "John Smith", id: "7") {
        grailsChange {
            init { ${GroovyChangeLogSpec.name}.calledBlocks << 'init' }
            validate { ${GroovyChangeLogSpec.name}.calledBlocks << 'validate' }
            change { ${GroovyChangeLogSpec.name}.calledBlocks << 'change' }
            rollback { ${GroovyChangeLogSpec.name}.calledBlocks << 'rollback' }
            confirm 'confirmation message'
            checkSum 'override value for checksum'
        }
    }
}
"""
        createCommand(DbmUpdateCountCommand).handle(getExecutionContext(DbmUpdateCountCommand, '1'))
        createCommand(DbmTagCommand).handle(getExecutionContext(DbmTagCommand, 'test tag'))
        createCommand(DbmChangelogSyncCommand).handle(getExecutionContext(DbmChangelogSyncCommand))
        calledBlocks = []

        when:
        command.handle(getExecutionContext(DbmRollbackCommand, 'test tag'))

        then:
        calledBlocks == ['init', 'change', 'rollback', 'rollback']
    }


    def "can use bind variables in the rollback block"() {
        given:
        def command = createCommand(DbmRollbackCommand)
        command.changeLogFile << """
databaseChangeLog = {
    changeSet(author: "John Smith", id: "8") {
    }
    changeSet(author: "John Smith", id: "9") {
        grailsChange {
            rollback {
                assert changeSet.id == '9'
                assert resourceAccessor.toString().startsWith('CompositeResourceAccessor{')
                assert ctx.hashCode() == ${applicationContext.hashCode()}
                assert application.hashCode() == ${applicationContext.getBean(GrailsApplication).hashCode()}
                ${GroovyChangeLogSpec.name}.calledBlocks << 'rollback'
            }
        }
    }
}
"""
        createCommand(DbmUpdateCountCommand).handle(getExecutionContext(DbmUpdateCountCommand, '1'))
        createCommand(DbmTagCommand).handle(getExecutionContext(DbmTagCommand, 'test tag'))
        createCommand(DbmChangelogSyncCommand).handle(getExecutionContext(DbmChangelogSyncCommand))
        calledBlocks = []

        when:
        command.handle(getExecutionContext(DbmRollbackCommand, 'test tag'))

        then:
        calledBlocks == ['rollback', 'rollback']
    }
}
