// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build go1.23

package main

import (
	"archive/zip"
	"bytes"
	"debug/buildinfo"
	"flag"
	"io"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
)

var flagUpdate = flag.Bool("update", false, "update golden files")

var moduleRoot string

func TestMain(m *testing.M) {
	cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}")
	cmd.Env = append(os.Environ(), "GOWORK=off")
	out, err := cmd.Output()
	if err != nil {
		log.Fatalf("failed to get module root for testing: %v\n", err)
	}
	moduleRoot = string(bytes.TrimSpace(out))
	os.Exit(m.Run())
}

func TestRelease(t *testing.T) {
	for _, fullCommand := range []string{
		"build-vscgo -out=/tmp/artifacts",
		"package -out=/tmp/artifacts",
		"publish -in=/tmp/artifacts",
	} {
		args := strings.Fields(fullCommand)
		// v0.43.0: prerelease
		// v0.44.0-rc.1: release candidate of stable release
		// v0.44.0: stable release
		// TODO(hyangah): skip rc in favor of prerelease versions.
		for _, tagName := range []string{"v0.43.0", "v0.44.0-rc.1", "v0.44.0"} {
			t.Run(args[0]+"-"+tagName, func(t *testing.T) {
				testRelease(t, moduleRoot, args[0], tagName, args[1:]...)
			})
		}
	}
}

func testRelease(t *testing.T, moduleRoot, command, tagName string, extraArgs ...string) {
	args := []string{"run", "-C", moduleRoot, "tools/release/release.go", command, "-n"}
	args = append(args, extraArgs...)

	cmd := exec.Command("go", args...)
	cmd.Env = append(os.Environ(),
		// Provide dummy environment variables required to run release.go commands.
		"TAG_NAME="+tagName,  // release tag
		"GITHUB_TOKEN=dummy", // github token needed to post release notes
		"VSCE_PAT=dummy",     // vsce token needed to publish the extension
		"COMMIT_SHA=4893cd984d190bdf2cd65e11c425b42819ae6f57", // bogus commit SHA used to post release notes
	)
	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("failed to run release %s: %v\n%s", command, err, output)
	}
	if *flagUpdate {
		if err := os.WriteFile(filepath.Join("testdata", command+"-"+tagName+".golden"), output, 0644); err != nil {
			t.Fatal("failed to write golden file:", err)
		}
		return
	}
	golden, err := os.ReadFile(filepath.Join("testdata", command+"-"+tagName+".golden"))
	if err != nil {
		t.Fatal("failed to read golden file:", err)
	}
	if diff := cmp.Diff(golden, output); diff != "" {
		t.Error("release package output mismatch (-want +got):\n", diff)
	}
}

func TestBuildVSCGO(t *testing.T) {
	modulePath := "github.com/golang/vscode-go"
	version := "v0.0.1"
	proxyURI := createTestModuleProxy(t, modulePath, version)

	gomodcache := t.TempDir()
	t.Cleanup(func() {
		cleanModuleCache(t, gomodcache)
	})

	outDir := t.TempDir()
	cmd := exec.Command("go", "run", "-C", moduleRoot, "tools/release/release.go", "build-vscgo", "-out", outDir)
	cmd.Env = append(os.Environ(),
		"GOPROXY="+proxyURI,
		"GOMODCACHE="+gomodcache,
		"GONOSUMDB="+modulePath,
		"TAG_NAME="+version,
	)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Fatalf("failed to run build-vscgo: %v\n%s", err, out)
	}

	file, err := os.Open(filepath.Join(outDir, "vscgo.zip"))
	if err != nil {
		t.Fatal(err)
	}
	defer file.Close()

	stat, err := file.Stat()
	if err != nil {
		t.Fatal(err)
	}
	zipReader, err := zip.NewReader(file, stat.Size())
	if err != nil {
		t.Fatal(err)
	}

	want := map[string]bool{}
	for _, platform := range targetPlatforms {
		want[platform.goos+"_"+platform.goarch] = true
	}
	for _, f := range zipReader.File {
		dirname := path.Base(path.Dir(f.Name))
		if !want[dirname] {
			t.Errorf("unexpected file in zip: %v", f.Name)
			continue
		}
		// dirname must encode goos/goarch
		goos, goarch, _ := strings.Cut(path.Base(path.Dir(f.Name)), "_")

		bin, err := f.Open()
		if err != nil {
			t.Fatal(err)
		}
		defer bin.Close()
		data, err := io.ReadAll(bin)
		if err != nil {
			t.Fatal(err)
		}
		bi, err := buildinfo.Read(bytes.NewReader(data))
		if err != nil {
			t.Fatal(err)
		}
		var gotGOOS, gotGOARCH string
		for _, s := range bi.Settings {
			switch s.Key {
			case "GOOS":
				gotGOOS = s.Value
			case "GOARCH":
				gotGOARCH = s.Value
			}
		}
		if bi.Path != "github.com/golang/vscode-go/vscgo" ||
			bi.Main.Path != "github.com/golang/vscode-go" ||
			bi.Main.Version != version ||
			gotGOOS != goos ||
			gotGOARCH != goarch {
			t.Errorf("%v: got %+v; want GOOS=%v/GOARCH=%v", f.Name, bi, goos, goarch)
		}
	}
}

func createTestModuleProxy(t *testing.T, modulePath string, version string) string {
	dirPath := modulePath + "@" + version + "/"
	files := map[string][]byte{
		dirPath + "go.mod":        []byte("module " + modulePath + "\n\ngo 1.20\n"),
		dirPath + "vscgo/main.go": []byte("package main\n\nfunc main() {\n\tprintln(\"hello world\")\n}"),
	}
	proxyURI, err := WriteProxy(t.TempDir(), files)
	if err != nil {
		t.Fatal("failed to write proxy:", err)
	}
	return proxyURI
}

func cleanModuleCache(t *testing.T, gomodcache string) {
	cmd := exec.Command("go", "clean", "-modcache")
	cmd.Env = append(os.Environ(), "GOMODCACHE="+gomodcache)
	if err := cmd.Run(); err != nil {
		t.Errorf("failed to clean module cache: %v\n", err)
	}
}
