package main_test

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/osbuild/images/pkg/arch"
	"github.com/osbuild/images/pkg/cloud"
	"github.com/osbuild/images/pkg/cloud/awscloud"
	"github.com/osbuild/images/pkg/platform"

	main "github.com/osbuild/image-builder-cli/cmd/image-builder"
	"github.com/osbuild/image-builder-cli/internal/testutil"
)

type fakeAwsUploader struct {
	checkCalls int

	uploadAndRegisterRead  bytes.Buffer
	uploadAndRegisterCalls int
	uploadAndRegisterErr   error
}

var _ = cloud.Uploader(&fakeAwsUploader{})

func (fa *fakeAwsUploader) Check(status io.Writer) error {
	fa.checkCalls++
	return nil
}

func (fa *fakeAwsUploader) UploadAndRegister(r io.Reader, status io.Writer) error {
	fa.uploadAndRegisterCalls++
	_, err := io.Copy(&fa.uploadAndRegisterRead, r)
	if err != nil {
		panic(err)
	}
	return fa.uploadAndRegisterErr
}

func TestUploadWithAWSMock(t *testing.T) {
	fakeDiskContent := "fake-raw-img"

	var regionName, bucketName, amiName string
	var uploadOpts *awscloud.UploaderOptions

	for _, tc := range []struct {
		fakeDiskName       string
		targetArchArg      string
		expectedUploadArch string
		expectedWarning    string
	}{
		// simple case: explicit target arch, no warning
		{"fake-disk.img", "aarch64", "aarch64", ""},
		// no target arch, detectable from filename: add note
		{"centos-9-ami-aarch64.img", "", "aarch64", `Note: using architecture "aarch64" based on image filename (use --arch to override)` + "\n"},
		// no target arch, not detectable form filename: we warn and expect host arch
		{"fake-disk.img", "", arch.Current().String(), fmt.Sprintf("WARNING: no upload architecture specified, using %q (use --arch to override)\n", arch.Current().String())},
	} {
		fakeImageFilePath := filepath.Join(t.TempDir(), tc.fakeDiskName)
		err := os.WriteFile(fakeImageFilePath, []byte(fakeDiskContent), 0644)
		assert.NoError(t, err)

		var fa fakeAwsUploader
		restore := main.MockAwscloudNewUploader(func(region string, bucket string, ami string, opts *awscloud.UploaderOptions) (cloud.Uploader, error) {
			regionName = region
			bucketName = bucket
			amiName = ami
			uploadOpts = opts
			return &fa, nil
		})
		defer restore()

		var fakeStdout, fakeStderr bytes.Buffer
		restore = main.MockOsStdout(&fakeStdout)
		defer restore()
		restore = main.MockOsStderr(&fakeStderr)
		defer restore()

		restore = main.MockOsArgs([]string{
			"upload",
			"--to=aws",
			"--aws-region=aws-region-1",
			"--aws-bucket=aws-bucket-2",
			"--aws-ami-name=aws-ami-3",
			"--arch=" + tc.targetArchArg,
			fakeImageFilePath,
		})
		defer restore()

		err = main.Run()
		require.NoError(t, err)

		assert.Equal(t, regionName, "aws-region-1")
		assert.Equal(t, bucketName, "aws-bucket-2")
		assert.Equal(t, amiName, "aws-ami-3")
		expectedBootMode := platform.BOOT_HYBRID
		targetArch, err := arch.FromString(tc.expectedUploadArch)
		assert.NoError(t, err)
		assert.Equal(t, &awscloud.UploaderOptions{TargetArch: targetArch, BootMode: &expectedBootMode}, uploadOpts)

		assert.Equal(t, 0, fa.checkCalls)
		assert.Equal(t, 1, fa.uploadAndRegisterCalls)
		assert.Equal(t, fakeDiskContent, fa.uploadAndRegisterRead.String())
		// progress was rendered
		assert.Contains(t, fakeStdout.String(), "--] 100.00%")

		// warning was passed
		assert.Equal(t, fakeStderr.String(), tc.expectedWarning)

	}
}

func TestUploadCmdlineErrors(t *testing.T) {
	var fakeStderr bytes.Buffer
	restore := main.MockOsStderr(&fakeStderr)
	defer restore()

	for _, tc := range []struct {
		cmdline     []string
		expectedErr string
	}{
		{
			nil,
			`missing --to parameter, try --to=aws`,
		}, {
			[]string{"--to=aws"},
			`missing all upload configuration: ["--aws-ami-name" "--aws-bucket" "--aws-region"]`,
		},
		{
			[]string{"--to=aws", "--aws-ami-name=1"},
			`missing upload configuration: ["--aws-bucket" "--aws-region"]`,
		},
		{
			[]string{"--to=aws", "--aws-ami-name=1", "--aws-bucket=2"},
			`missing upload configuration: ["--aws-region"]`,
		},
	} {
		t.Run(strings.Join(tc.cmdline, ","), func(t *testing.T) {
			cmd := append([]string{"upload"}, tc.cmdline...)
			cmd = append(cmd, "/path/to/some/image")
			restore := main.MockOsArgs(cmd)
			defer restore()

			err := main.Run()
			require.EqualError(t, err, tc.expectedErr)
		})
	}
}

func TestBuildAndUploadWithAWSMock(t *testing.T) {
	if testing.Short() {
		t.Skip("manifest generation takes a while")
	}
	if !hasDepsolveDnf() {
		t.Skip("no osbuild-depsolve-dnf binary found")
	}

	var regionName, bucketName, amiName string
	var fa fakeAwsUploader
	var uploadOpts *awscloud.UploaderOptions
	restore := main.MockAwscloudNewUploader(func(region string, bucket string, ami string, opts *awscloud.UploaderOptions) (cloud.Uploader, error) {
		regionName = region
		bucketName = bucket
		amiName = ami
		uploadOpts = opts
		return &fa, nil
	})
	defer restore()

	outputDir := t.TempDir()
	fakeOsbuildScript := makeFakeOsbuildScript()
	testutil.MockCommand(t, "osbuild", fakeOsbuildScript)

	var fakeStdout bytes.Buffer
	restore = main.MockOsStdout(&fakeStdout)
	defer restore()

	restore = main.MockOsArgs([]string{
		"build",
		"--output-dir", outputDir,
		"--aws-region=aws-region-1",
		"--aws-bucket=aws-bucket-2",
		"--aws-ami-name=aws-ami-3",
		"ami",
		"--distro=centos-9",
	})
	defer restore()

	err := main.Run()
	require.NoError(t, err)

	assert.Equal(t, regionName, "aws-region-1")
	assert.Equal(t, bucketName, "aws-bucket-2")
	assert.Equal(t, amiName, "aws-ami-3")
	expectedBootMode := platform.BOOT_HYBRID
	assert.Equal(t, &awscloud.UploaderOptions{BootMode: &expectedBootMode, TargetArch: arch.Current()}, uploadOpts)
	assert.Equal(t, 1, fa.checkCalls)
	assert.Equal(t, 1, fa.uploadAndRegisterCalls)
	assert.Equal(t, "fake-img-raw\n", fa.uploadAndRegisterRead.String())
}

func TestBuildAmiButNotUpload(t *testing.T) {
	if testing.Short() {
		t.Skip("manifest generation takes a while")
	}
	if !hasDepsolveDnf() {
		t.Skip("no osbuild-depsolve-dnf binary found")
	}

	fa := fakeAwsUploader{
		uploadAndRegisterErr: fmt.Errorf("upload should not be called"),
	}
	restore := main.MockAwscloudNewUploader(func(region string, bucket string, ami string, opts *awscloud.UploaderOptions) (cloud.Uploader, error) {
		return &fa, nil
	})
	defer restore()

	outputDir := t.TempDir()
	fakeOsbuildScript := makeFakeOsbuildScript()
	testutil.MockCommand(t, "osbuild", fakeOsbuildScript)

	var fakeStdout bytes.Buffer
	restore = main.MockOsStdout(&fakeStdout)
	defer restore()

	restore = main.MockOsArgs([]string{
		"build",
		"--output-dir", outputDir,
		"ami",
		"--distro=centos-9",
	})
	defer restore()

	err := main.Run()
	require.NoError(t, err)

	assert.Equal(t, 0, fa.uploadAndRegisterCalls)
}

func TestBuildAndUploadWithAWSPartialCmdlineErrors(t *testing.T) {
	if testing.Short() {
		t.Skip("manifest generation takes a while")
	}
	if !hasDepsolveDnf() {
		t.Skip("no osbuild-depsolve-dnf binary found")
	}

	outputDir := t.TempDir()
	fakeOsbuildScript := makeFakeOsbuildScript()
	testutil.MockCommand(t, "osbuild", fakeOsbuildScript)

	restore := main.MockOsArgs([]string{
		"build",
		"--output-dir", outputDir,
		// note that --aws-{ami-name,bucket} is missing
		"--aws-region=aws-region-1",
		"ami",
		"--distro=centos-9",
	})
	defer restore()

	err := main.Run()
	assert.EqualError(t, err, `missing upload configuration: ["--aws-ami-name" "--aws-bucket"]`)
}
