/*
 * Copyright 2013-2026, Seqera Labs
 *
 * Licensed 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
 *
 *     http://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 nextflow.cli.module

import io.seqera.npr.client.RegistryClient
import io.seqera.npr.api.schema.v1.Module
import io.seqera.npr.api.schema.v1.ModuleRelease
import nextflow.cli.CliOptions
import nextflow.cli.Launcher
import nextflow.exception.AbortOperationException
import nextflow.module.ModuleReference
import nextflow.module.ModuleStorage
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
import org.junit.Rule
import spock.lang.Specification
import spock.lang.TempDir
import test.OutputCapture

import java.nio.file.Files
import java.nio.file.Path
import java.util.regex.Pattern
import java.util.zip.GZIPOutputStream

/**
 * Tests for CmdModuleRun command
 *
 * @author Jorge Ejarque <jorge.ejarque@seqera.io>
 */
class CmdModuleRunTest extends Specification {

    @Rule
    OutputCapture capture = new OutputCapture()

    @TempDir
    Path tempDir

    def 'should run module and create output file'() {
        given:
        // Create a simple module script that creates a file
        def moduleScript = '''
            process CREATE_FILE {
                output:
                path "test_output.txt"

                script:
                """
                echo "Module executed successfully" > test_output.txt
                """
            }
        '''.stripIndent()

        and:
        // Create module directory structure
        def storage = new ModuleStorage(tempDir)
        def moduleRef = new ModuleReference('nf-core', 'test-module')
        def moduleDir = storage.getModuleDir(moduleRef)
        Files.createDirectories(moduleDir)

        // Write main.nf
        moduleDir.resolve('main.nf').text = moduleScript

        // Write meta.yml
        moduleDir.resolve('meta.yml').text = '''
            name: nf-core/test-module
            version: 1.0.0
            description: Test module that creates a file
        '''.stripIndent()

        and:
        // Create mock module package
        def modulePackage = createModulePackage(moduleScript)

        // Mock registry client
        def mockClient = Stub(RegistryClient)
        def moduleRelease = new ModuleRelease()
        moduleRelease.version = '1.0.0'
        def module = new Module()
        module.name = 'nf-core/test-module'
        module.latest = moduleRelease
        mockClient.getModule(_) >> module  // Use wildcard to match any argument
        mockClient.downloadModuleRelease(_, _, _) >> { String name, String version, Path dest ->
            Files.write(dest, modulePackage)
            return dest
        }
        def escapedPath = Pattern.quote(tempDir.toString())
        def pattern = ~/"${escapedPath}\/.+\/test_output\.txt"/

        and:
        def cmd = new CmdModuleRun()
        def opts = new CliOptions()
        opts.setQuiet(true)
        cmd.launcher = Mock(Launcher) {
            getOptions() >> opts
            getCliString() >> "nextflow module run nf-core/test-module"
        }
        cmd.args = ['nf-core/test-module']
        cmd.root = tempDir
        cmd.workDir = tempDir.toString()
        cmd.outputDir = tempDir.resolve('results').toString()
        cmd.outputFormat = 'json'
        cmd.client = mockClient

        when:
        cmd.run()
        def stdout = capture
            .toString()
            .readLines()// remove the log part
            .findResults { line -> !line.contains('DEBUG') ? line : null }
            .findResults { line -> !line.contains('INFO') ? line : null }.join(" ")

        then:
        assert (stdout =~ pattern).find()
        and:
        // Verify module was installed
        Files.exists(moduleDir)
        Files.exists(moduleDir.resolve('main.nf'))

    }

    def 'should run module with specific version'() {
        given:
        def moduleScript = '''
            process CREATE_FILE_V2 {
                output:
                path "test_output_v2.txt", emit: output_path

                script:
                """
                echo "Module version 2.0.0 executed successfully" > test_output_v2.txt
                """
            }
        '''

        and:
        def storage = new ModuleStorage(tempDir)
        def moduleRef = new ModuleReference('nf-core', 'test-module')
        def moduleDir = storage.getModuleDir(moduleRef)
        Files.createDirectories(moduleDir)
        moduleDir.resolve('main.nf').text = moduleScript
        moduleDir.resolve('meta.yml').text = 'name: nf-core/test-module\nversion: 2.0.0'
        def escapedPath = Pattern.quote(tempDir.toString())
        def pattern = ~/"output_path": "${escapedPath}\/.+\/test_output_v2\.txt"/

        and:
        def modulePackage = createModulePackage(moduleScript)

        def mockClient = Mock(RegistryClient)
        mockClient.downloadModuleRelease('nf-core/test-module', '2.0.0', _) >> { String name, String version, Path dest ->
            Files.write(dest, modulePackage)
            return dest
        }

        and:
        def cmd = new CmdModuleRun()
        def opts = new CliOptions()
        opts.setQuiet(true)
        cmd.launcher = Mock(Launcher) {
            getOptions() >> opts
            getCliString() >> "nextflow module run nf-core/test-module"
        }
        cmd.args = ['nf-core/test-module']
        cmd.version = '2.0.0'
        cmd.root = tempDir
        cmd.workDir = tempDir.toString()
        cmd.outputDir = tempDir.resolve('results').toString()
        cmd.outputFormat = 'json'
        cmd.client = mockClient

        when:
        cmd.run()

        then:
        def stdout = capture
            .toString()
            .readLines()// remove the log part
            .findResults { line -> !line.contains('DEBUG') ? line : null }
            .findResults { line -> !line.contains('INFO') ? line : null }
            .findResults { line -> !line.contains('plugin') ? line : null }.join(" ")
        assert (stdout =~ pattern).find()

    }

    def 'should run module from local path'() {
        given:
        def moduleScript = '''
            process CREATE_LOCAL_FILE {
                output:
                path "local_output.txt"

                script:
                """
                echo "Local module executed" > local_output.txt
                """
            }
        '''.stripIndent()

        and:
        // Create a local module directory with main.nf
        def moduleDir = tempDir.resolve('local-module')
        Files.createDirectories(moduleDir)
        moduleDir.resolve('main.nf').text = moduleScript

        def escapedPath = Pattern.quote(tempDir.toString())
        def pattern = ~/"${escapedPath}\/.+\/local_output\.txt"/

        and:
        def cmd = new CmdModuleRun()
        def opts = new CliOptions()
        opts.setQuiet(true)
        cmd.launcher = Mock(Launcher) {
            getOptions() >> opts
            getCliString() >> "nextflow module run ${moduleDir}"
        }
        cmd.args = [moduleDir.toString()]
        cmd.root = tempDir
        cmd.workDir = tempDir.toString()
        cmd.outputDir = tempDir.resolve('results').toString()
        cmd.outputFormat = 'json'

        when:
        cmd.run()
        def stdout = capture
            .toString()
            .readLines()// remove the log part
            .findResults { line -> !line.contains('DEBUG') ? line : null }
            .findResults { line -> !line.contains('INFO') ? line : null }.join(" ")

        then:
        assert (stdout =~ pattern).find()
    }

    def 'should fail when path and module do not exist'() {
        given:
        def nonExistentPath = tempDir.resolve('does-not-exist').toString()
        def cmd = new CmdModuleRun()
        cmd.launcher = Mock(Launcher) {
            getOptions() >> null
        }
        cmd.args = [nonExistentPath]
        cmd.root = tempDir

        when:
        cmd.run()

        then:
        def e = thrown(AbortOperationException)
        e.message.contains('Invalid module path')
    }

    def 'should fail with no arguments'() {
        given:
        def cmd = new CmdModuleRun()
        cmd.launcher = Mock(Launcher) {
            getOptions() >> null
        }
        cmd.args = []
        cmd.root = tempDir

        when:
        cmd.run()

        then:
        thrown(AbortOperationException)
    }

    def 'should fail with invalid module reference'() {
        given:
        def cmd = new CmdModuleRun()
        cmd.launcher = Mock(Launcher) {
            getOptions() >> null
        }
        cmd.args = ['invalid-module']  // Missing scope
        cmd.root = tempDir

        when:
        cmd.run()

        then:
        thrown(AbortOperationException)
    }

    // Helper method to create a module package (tar.gz)
    private byte[] createModulePackage(String mainNfContent) {
        def baos = new ByteArrayOutputStream()

        new GZIPOutputStream(baos).withCloseable { gzos ->
            new TarArchiveOutputStream(gzos).withCloseable { tos ->
                // Add main.nf
                addTarEntry(tos, 'main.nf', mainNfContent.bytes)

                // Add meta.yml
                def metaContent = '''
                    name: test-module
                    version: 1.0.0
                    description: Test module
                '''.stripIndent()
                addTarEntry(tos, 'meta.yml', metaContent.bytes)
            }
        }

        return baos.toByteArray()
    }

    private void addTarEntry(TarArchiveOutputStream tos, String name, byte[] content) {
        def entry = new TarArchiveEntry(name)
        entry.setSize(content.length)
        tos.putArchiveEntry(entry)
        tos.write(content)
        tos.closeArchiveEntry()
    }
}
