package build

import (
	"context"
	"sort"
	"testing"

	"github.com/containerd/platforms"
	"github.com/docker/buildx/builder"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/stretchr/testify/require"
)

func TestFindDriverSanity(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.DefaultSpec()},
	})

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.DefaultSpec()}, nil, platforms.OnlyStrict, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, 0, res[0].driverIndex)
	require.Equal(t, "aaa", res[0].Node().Builder)
	require.Equal(t, []ocispecs.Platform{platforms.DefaultSpec()}, res[0].platforms)
}

func TestFindDriverEmpty(t *testing.T) {
	r := makeTestResolver(nil)

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.DefaultSpec()}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Nil(t, res)
}

func TestFindDriverWeirdName(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64")},
		"bbb": {platforms.MustParse("linux/foobar")},
	})

	// find first platform
	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/foobar")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, 1, res[0].driverIndex)
	require.Equal(t, "bbb", res[0].Node().Builder)
}

func TestFindDriverUnknown(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64")},
	})

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/riscv64")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.False(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, 0, res[0].driverIndex)
	require.Equal(t, "aaa", res[0].Node().Builder)
}

func TestSelectNodeSinglePlatform(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64")},
		"bbb": {platforms.MustParse("linux/riscv64")},
	})

	// find first platform
	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/amd64")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, 0, res[0].driverIndex)
	require.Equal(t, "aaa", res[0].Node().Builder)

	// find second platform
	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/riscv64")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, 1, res[0].driverIndex)
	require.Equal(t, "bbb", res[0].Node().Builder)

	// find an unknown platform, should match the first driver
	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/s390x")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.False(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, 0, res[0].driverIndex)
	require.Equal(t, "aaa", res[0].Node().Builder)
}

func TestSelectNodeMultiPlatform(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64"), platforms.MustParse("linux/arm64")},
		"bbb": {platforms.MustParse("linux/riscv64")},
	})

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/amd64")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, 0, res[0].driverIndex)
	require.Equal(t, "aaa", res[0].Node().Builder)

	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm64")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, 0, res[0].driverIndex)
	require.Equal(t, "aaa", res[0].Node().Builder)

	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/riscv64")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, 1, res[0].driverIndex)
	require.Equal(t, "bbb", res[0].Node().Builder)
}

func TestSelectNodeNonStrict(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64")},
		"bbb": {platforms.MustParse("linux/arm64")},
	})

	// arm64 should match itself
	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm64")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "bbb", res[0].Node().Builder)

	// arm64 may support arm/v8
	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm/v8")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "bbb", res[0].Node().Builder)

	// arm64 may support arm/v7
	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "bbb", res[0].Node().Builder)
}

func TestSelectNodeNonStrictARM(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64")},
		"bbb": {platforms.MustParse("linux/arm64")},
		"ccc": {platforms.MustParse("linux/arm/v8")},
	})

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm/v8")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "ccc", res[0].Node().Builder)

	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "ccc", res[0].Node().Builder)
}

func TestSelectNodeNonStrictLower(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64")},
		"bbb": {platforms.MustParse("linux/arm/v7")},
	})

	// v8 can't be built on v7 (so we should select the default)...
	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm/v8")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.False(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "aaa", res[0].Node().Builder)

	// ...but v6 can be built on v8
	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm/v6")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "bbb", res[0].Node().Builder)
}

func TestSelectNodePreferStart(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64")},
		"bbb": {platforms.MustParse("linux/riscv64")},
		"ccc": {platforms.MustParse("linux/riscv64")},
	})

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/riscv64")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "bbb", res[0].Node().Builder)
}

func TestSelectNodePreferExact(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/arm/v8")},
		"bbb": {platforms.MustParse("linux/arm/v7")},
	})

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "bbb", res[0].Node().Builder)
}

func TestSelectNodeNoPlatform(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/foobar")},
		"bbb": {platforms.DefaultSpec()},
	})

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "aaa", res[0].Node().Builder)
	require.Empty(t, res[0].platforms)
}

func TestSelectNodeAdditionalPlatforms(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64")},
		"bbb": {platforms.MustParse("linux/arm/v8")},
	})

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "bbb", res[0].Node().Builder)

	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{platforms.MustParse("linux/arm/v7")}, nil, platforms.Only, func(idx int, n builder.Node) []ocispecs.Platform {
		if n.Builder == "aaa" {
			return []ocispecs.Platform{platforms.MustParse("linux/arm/v7")}
		}
		return nil
	})
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "aaa", res[0].Node().Builder)
}

func TestSplitNodeMultiPlatform(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64"), platforms.MustParse("linux/arm64")},
		"bbb": {platforms.MustParse("linux/riscv64")},
	})

	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{
		platforms.MustParse("linux/amd64"),
		platforms.MustParse("linux/arm64"),
	}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 1)
	require.Equal(t, "aaa", res[0].Node().Builder)

	res, perfect, err = r.resolve(context.TODO(), []ocispecs.Platform{
		platforms.MustParse("linux/amd64"),
		platforms.MustParse("linux/riscv64"),
	}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 2)
	require.Equal(t, "aaa", res[0].Node().Builder)
	require.Equal(t, "bbb", res[1].Node().Builder)
}

func TestSplitNodeMultiPlatformNoUnify(t *testing.T) {
	r := makeTestResolver(map[string][]ocispecs.Platform{
		"aaa": {platforms.MustParse("linux/amd64")},
		"bbb": {platforms.MustParse("linux/amd64"), platforms.MustParse("linux/riscv64")},
	})

	// the "best" choice would be the node with both platforms, but we're using
	// a naive algorithm that doesn't try to unify the platforms
	res, perfect, err := r.resolve(context.TODO(), []ocispecs.Platform{
		platforms.MustParse("linux/amd64"),
		platforms.MustParse("linux/riscv64"),
	}, nil, platforms.Only, nil)
	require.NoError(t, err)
	require.True(t, perfect)
	require.Len(t, res, 2)
	require.Equal(t, "aaa", res[0].Node().Builder)
	require.Equal(t, "bbb", res[1].Node().Builder)
}

func makeTestResolver(nodes map[string][]ocispecs.Platform) *nodeResolver {
	var ns []builder.Node
	for name, platforms := range nodes {
		ns = append(ns, builder.Node{
			Builder:   name,
			Platforms: platforms,
		})
	}
	sort.Slice(ns, func(i, j int) bool {
		return ns[i].Builder < ns[j].Builder
	})
	return newDriverResolver(ns)
}
