package ko

import (
	"fmt"
	"strconv"
	"strings"
	"testing"
	"time"

	_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
	_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
	"github.com/google/go-containerregistry/pkg/name"
	"github.com/google/go-containerregistry/pkg/v1/remote"
	"github.com/goreleaser/goreleaser/internal/testctx"
	"github.com/goreleaser/goreleaser/internal/testlib"
	"github.com/goreleaser/goreleaser/pkg/config"
	"github.com/goreleaser/goreleaser/pkg/context"
	"github.com/stretchr/testify/require"
)

const (
	registryPort = "5052"
	registry     = "localhost:5052/"
)

func TestDefault(t *testing.T) {
	ctx := testctx.NewWithCfg(config.Project{
		Env: []string{
			"KO_DOCKER_REPO=" + registry,
			"COSIGN_REPOSITORY=" + registry,
			"LDFLAGS=foobar",
			"FLAGS=barfoo",
			"LE_ENV=test",
		},
		ProjectName: "test",
		Builds: []config.Build{
			{
				ID:  "test",
				Dir: ".",
				BuildDetails: config.BuildDetails{
					Ldflags: []string{"{{.Env.LDFLAGS}}"},
					Flags:   []string{"{{.Env.FLAGS}}"},
					Env:     []string{"SOME_ENV={{.Env.LE_ENV}}"},
				},
			},
		},
		Kos: []config.Ko{
			{},
		},
	})
	require.NoError(t, Pipe{}.Default(ctx))
	require.Equal(t, config.Ko{
		ID:         "test",
		Build:      "test",
		BaseImage:  chainguardStatic,
		Repository: registry,
		Platforms:  []string{"linux/amd64"},
		SBOM:       "spdx",
		Tags:       []string{"latest"},
		WorkingDir: ".",
		Ldflags:    []string{"{{.Env.LDFLAGS}}"},
		Flags:      []string{"{{.Env.FLAGS}}"},
		Env:        []string{"SOME_ENV={{.Env.LE_ENV}}"},
	}, ctx.Config.Kos[0])
}

func TestDefaultNoImage(t *testing.T) {
	ctx := testctx.NewWithCfg(config.Project{
		ProjectName: "test",
		Builds: []config.Build{
			{
				ID: "test",
			},
		},
		Kos: []config.Ko{
			{},
		},
	})
	require.ErrorIs(t, Pipe{}.Default(ctx), errNoRepository)
}

func TestDescription(t *testing.T) {
	require.NotEmpty(t, Pipe{}.String())
}

func TestSkip(t *testing.T) {
	t.Run("skip ko set", func(t *testing.T) {
		ctx := testctx.NewWithCfg(config.Project{
			Kos: []config.Ko{{}},
		})
		ctx.SkipKo = true
		require.True(t, Pipe{}.Skip(ctx))
	})
	t.Run("skip no kos", func(t *testing.T) {
		ctx := testctx.New()
		require.True(t, Pipe{}.Skip(ctx))
	})
	t.Run("dont skip", func(t *testing.T) {
		ctx := testctx.NewWithCfg(config.Project{
			Kos: []config.Ko{{}},
		})
		require.False(t, Pipe{}.Skip(ctx))
	})
}

func TestPublishPipeNoMatchingBuild(t *testing.T) {
	ctx := testctx.NewWithCfg(config.Project{
		Builds: []config.Build{
			{
				ID: "doesnt matter",
			},
		},
		Kos: []config.Ko{
			{
				ID:    "default",
				Build: "wont match nothing",
			},
		},
	})

	require.EqualError(t, Pipe{}.Default(ctx), `no builds with id "wont match nothing"`)
}

func TestPublishPipeSuccess(t *testing.T) {
	testlib.StartRegistry(t, "ko_registry", registryPort)

	table := []struct {
		Name               string
		SBOM               string
		BaseImage          string
		Labels             map[string]string
		ExpectedLabels     map[string]string
		Platforms          []string
		CreationTime       string
		KoDataCreationTime string
	}{
		{
			// Must be first as others add an SBOM for the same image
			Name: "sbom-none",
			SBOM: "none",
		},
		{
			Name: "sbom-spdx",
			SBOM: "spdx",
		},
		{
			Name: "sbom-cyclonedx",
			SBOM: "cyclonedx",
		},
		{
			Name: "sbom-go.version-m",
			SBOM: "go.version-m",
		},
		{
			Name:      "base-image-is-not-index",
			BaseImage: "alpine:latest@sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
		},
		{
			Name:      "multiple-platforms",
			Platforms: []string{"linux/amd64", "linux/arm64"},
		},
		{
			Name:           "labels",
			Labels:         map[string]string{"foo": "bar", "project": "{{.ProjectName}}"},
			ExpectedLabels: map[string]string{"foo": "bar", "project": "test"},
		},
		{
			Name:         "creation-time",
			CreationTime: "1672531200",
		},
		{
			Name:               "kodata-creation-time",
			KoDataCreationTime: "1672531200",
		},
	}

	repository := fmt.Sprintf("%sgoreleasertest/testapp", registry)

	for _, table := range table {
		t.Run(table.Name, func(t *testing.T) {
			ctx := testctx.NewWithCfg(config.Project{
				ProjectName: "test",
				Builds: []config.Build{
					{
						ID: "foo",
						BuildDetails: config.BuildDetails{
							Ldflags: []string{"-s", "-w"},
							Flags:   []string{"-tags", "netgo"},
							Env:     []string{"GOCACHE=" + t.TempDir()},
						},
					},
				},
				Kos: []config.Ko{
					{
						ID:                 "default",
						Build:              "foo",
						WorkingDir:         "./testdata/app/",
						BaseImage:          table.BaseImage,
						Repository:         repository,
						Labels:             table.Labels,
						Platforms:          table.Platforms,
						Tags:               []string{table.Name},
						CreationTime:       table.CreationTime,
						KoDataCreationTime: table.KoDataCreationTime,
						SBOM:               table.SBOM,
						Bare:               true,
					},
				},
			})

			require.NoError(t, Pipe{}.Default(ctx))
			require.NoError(t, Pipe{}.Publish(ctx))

			ref, err := name.ParseReference(
				fmt.Sprintf("%s:%s", repository, table.Name),
				name.Insecure,
			)
			require.NoError(t, err)

			index, err := remote.Index(ref)
			if len(table.Platforms) > 1 {
				require.NoError(t, err)
				imf, err := index.IndexManifest()
				require.NoError(t, err)

				platforms := make([]string, 0, len(imf.Manifests))
				for _, mf := range imf.Manifests {
					platforms = append(platforms, mf.Platform.String())
				}
				require.ElementsMatch(t, table.Platforms, platforms)
			} else {
				require.Error(t, err)
			}

			image, err := remote.Image(ref)
			require.NoError(t, err)

			digest, err := image.Digest()
			require.NoError(t, err)

			sbomRef, err := name.ParseReference(
				fmt.Sprintf(
					"%s:%s.sbom",
					repository,
					strings.Replace(digest.String(), ":", "-", 1),
				),
				name.Insecure,
			)
			require.NoError(t, err)

			sbom, err := remote.Image(sbomRef)
			if table.SBOM == "none" {
				require.Error(t, err)
			} else {
				require.NoError(t, err)

				layers, err := sbom.Layers()
				require.NoError(t, err)
				require.NotEmpty(t, layers)

				mediaType, err := layers[0].MediaType()
				require.NoError(t, err)

				switch table.SBOM {
				case "spdx", "":
					require.Equal(t, "text/spdx+json", string(mediaType))
				case "cyclonedx":
					require.Equal(t, "application/vnd.cyclonedx+json", string(mediaType))
				case "go.version-m":
					require.Equal(t, "application/vnd.go.version-m", string(mediaType))
				default:
					require.Fail(t, "unknown SBOM type", table.SBOM)
				}
			}

			configFile, err := image.ConfigFile()
			require.NoError(t, err)
			require.GreaterOrEqual(t, len(configFile.History), 3)

			require.Equal(t, table.ExpectedLabels, configFile.Config.Labels)

			var creationTime time.Time
			if table.CreationTime != "" {
				ct, err := strconv.ParseInt(table.CreationTime, 10, 64)
				require.NoError(t, err)
				creationTime = time.Unix(ct, 0).UTC()

				require.Equal(t, creationTime, configFile.Created.Time.UTC())
			}
			require.Equal(t, creationTime, configFile.History[len(configFile.History)-1].Created.Time.UTC())

			var koDataCreationTime time.Time
			if table.KoDataCreationTime != "" {
				kdct, err := strconv.ParseInt(table.KoDataCreationTime, 10, 64)
				require.NoError(t, err)
				koDataCreationTime = time.Unix(kdct, 0).UTC()
			}
			require.Equal(t, koDataCreationTime, configFile.History[len(configFile.History)-2].Created.Time.UTC())
		})
	}
}

func TestPublishPipeError(t *testing.T) {
	makeCtx := func() *context.Context {
		return testctx.NewWithCfg(config.Project{
			Builds: []config.Build{
				{
					ID:   "foo",
					Main: "./...",
				},
			},
			Kos: []config.Ko{
				{
					ID:         "default",
					Build:      "foo",
					WorkingDir: "./testdata/app/",
					Repository: "fakerepo:8080/",
					Tags:       []string{"latest", "{{.Tag}}"},
				},
			},
		}, testctx.WithCurrentTag("v1.0.0"))
	}

	t.Run("invalid base image", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Kos[0].BaseImage = "not a valid image hopefully"
		require.NoError(t, Pipe{}.Default(ctx))
		require.EqualError(t, Pipe{}.Publish(ctx), `build: could not parse reference: not a valid image hopefully`)
	})

	t.Run("invalid label tmpl", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Kos[0].Labels = map[string]string{"nope": "{{.Nope}}"}
		require.NoError(t, Pipe{}.Default(ctx))
		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
	})

	t.Run("invalid sbom", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Kos[0].SBOM = "nope"
		require.NoError(t, Pipe{}.Default(ctx))
		require.EqualError(t, Pipe{}.Publish(ctx), `makeBuilder: unknown sbom type: "nope"`)
	})

	t.Run("invalid build", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Kos[0].WorkingDir = t.TempDir()
		require.NoError(t, Pipe{}.Default(ctx))
		require.EqualError(t, Pipe{}.Publish(ctx), `build: exit status 1`)
	})

	t.Run("invalid tags tmpl", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Kos[0].Tags = []string{"{{.Nope}}"}
		require.NoError(t, Pipe{}.Default(ctx))
		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
	})

	t.Run("invalid creation time", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Kos[0].CreationTime = "nope"
		require.NoError(t, Pipe{}.Default(ctx))
		err := Pipe{}.Publish(ctx)
		require.Error(t, err)
		require.Contains(t, err.Error(), `strconv.ParseInt: parsing "nope": invalid syntax`)
	})

	t.Run("invalid creation time tmpl", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Kos[0].CreationTime = "{{.Nope}}"
		require.NoError(t, Pipe{}.Default(ctx))
		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
	})

	t.Run("invalid kodata creation time", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Kos[0].KoDataCreationTime = "nope"
		require.NoError(t, Pipe{}.Default(ctx))
		err := Pipe{}.Publish(ctx)
		require.Error(t, err)
		require.Contains(t, err.Error(), `strconv.ParseInt: parsing "nope": invalid syntax`)
	})

	t.Run("invalid kodata creation time tmpl", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Kos[0].KoDataCreationTime = "{{.Nope}}"
		require.NoError(t, Pipe{}.Default(ctx))
		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
	})

	t.Run("invalid env tmpl", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Builds[0].Env = []string{"{{.Nope}}"}
		require.NoError(t, Pipe{}.Default(ctx))
		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
	})

	t.Run("invalid ldflags tmpl", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Builds[0].Ldflags = []string{"{{.Nope}}"}
		require.NoError(t, Pipe{}.Default(ctx))
		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
	})

	t.Run("invalid flags tmpl", func(t *testing.T) {
		ctx := makeCtx()
		ctx.Config.Builds[0].Flags = []string{"{{.Nope}}"}
		require.NoError(t, Pipe{}.Default(ctx))
		testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
	})

	t.Run("publish fail", func(t *testing.T) {
		ctx := makeCtx()
		require.NoError(t, Pipe{}.Default(ctx))
		err := Pipe{}.Publish(ctx)
		require.Error(t, err)
		require.Contains(t, err.Error(), `publish: writing sbom: Get "https://fakerepo:8080/v2/": dial tcp:`)
	})
}

func TestApplyTemplate(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		foo, err := applyTemplate(testctx.NewWithCfg(config.Project{
			Env: []string{"FOO=bar"},
		}), []string{"{{ .Env.FOO }}"})
		require.NoError(t, err)
		require.Equal(t, []string{"bar"}, foo)
	})
	t.Run("error", func(t *testing.T) {
		_, err := applyTemplate(testctx.New(), []string{"{{ .Nope}}"})
		require.Error(t, err)
	})
}
