//go:build smoke

package smoketest

import (
	"archive/tar"
	"compress/gzip"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
)

// Release-server state set up lazily by prepareSmokeReleases. The HTTP
// server stays up for the duration of TestMain (shut down in teardown).
// smokeRelOnce serializes concurrent first-callers; smokeRelErr is the
// stored result for replay so subsequent callers see the same outcome.
var (
	smokeRelOnce      sync.Once
	smokeRelErr       error
	manifestURL       string
	pubkeyFile        string
	releaseHTTPServer *httptest.Server
	releaseRelDir     string
	smokeRelKey       *ecdsa.PrivateKey
)

const (
	smokeReleaseGood   = "v0.smoke.0"
	smokeReleaseBroken = "v0.smoke.broken-bangerd"
)

// prepareSmokeReleases is the Go port of scripts/smoke.sh's
// prepare_smoke_releases. It generates an ECDSA P-256 keypair (matching
// cosign blob signatures, which are ASN.1 DER ECDSA over SHA256(body),
// base64-encoded), builds two coverage-instrumented release tarballs
// signed with that key, writes a manifest, and stands up an httptest
// file server. The hidden --manifest-url / --pubkey-file flags on
// `banger update` redirect the updater at this fake bucket.
//
// Idempotent. The first caller pays the build/server cost; later
// callers replay the cached result.
func prepareSmokeReleases() error {
	smokeRelOnce.Do(func() {
		smokeRelErr = doPrepareSmokeReleases()
	})
	return smokeRelErr
}

func doPrepareSmokeReleases() error {
	releaseRelDir = filepath.Join(scratchRoot, "release")
	if err := os.RemoveAll(releaseRelDir); err != nil {
		return fmt.Errorf("clean release dir: %w", err)
	}
	if err := os.MkdirAll(releaseRelDir, 0o755); err != nil {
		return fmt.Errorf("mkdir release dir: %w", err)
	}

	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return fmt.Errorf("generate ECDSA key: %w", err)
	}
	smokeRelKey = priv

	pubDER, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
	if err != nil {
		return fmt.Errorf("marshal pub key: %w", err)
	}
	pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})
	pubkeyFile = filepath.Join(releaseRelDir, "cosign.pub")
	if err := os.WriteFile(pubkeyFile, pubPEM, 0o644); err != nil {
		return fmt.Errorf("write pub key: %w", err)
	}

	if err := buildSmokeReleaseTarball(smokeReleaseGood); err != nil {
		return err
	}
	if err := buildSmokeReleaseTarball(smokeReleaseBroken); err != nil {
		return err
	}

	releaseHTTPServer = httptest.NewServer(http.FileServer(http.Dir(releaseRelDir)))
	manifestPath := filepath.Join(releaseRelDir, "manifest.json")
	if err := writeSmokeManifest(manifestPath, releaseHTTPServer.URL); err != nil {
		return err
	}
	manifestURL = releaseHTTPServer.URL + "/manifest.json"
	return nil
}

func shutdownReleaseServer() {
	if releaseHTTPServer != nil {
		releaseHTTPServer.Close()
	}
}

// buildSmokeReleaseTarball is the Go port of build_smoke_release_tarball
// from scripts/smoke.sh. It compiles banger / bangerd / banger-vsock-agent
// with the requested Version baked in, packages them as a gzip tarball,
// and writes SHA256SUMS + SHA256SUMS.sig alongside.
//
// The v0.smoke.broken-* family ships a shell-script bangerd that passes
// `--check-migrations` (so the swap proceeds) but exits non-zero in
// service mode (so the post-swap restart fails and rollbackAndWrap
// fires). Same trick the bash version uses.
func buildSmokeReleaseTarball(version string) error {
	outDir := filepath.Join(releaseRelDir, version)
	stage := filepath.Join(outDir, ".stage")
	if err := os.MkdirAll(stage, 0o755); err != nil {
		return fmt.Errorf("mkdir stage: %w", err)
	}

	ldflags := "-X banger/internal/buildinfo.Version=" + version +
		" -X banger/internal/buildinfo.Commit=smoke" +
		" -X banger/internal/buildinfo.BuiltAt=2026-04-30T00:00:00Z"

	root, err := repoRoot()
	if err != nil {
		return err
	}

	build := func(target, output string, extraEnv ...string) error {
		cmd := exec.Command("go", "build", "-ldflags", ldflags, "-o", output, target)
		cmd.Dir = root
		if len(extraEnv) > 0 {
			cmd.Env = append(os.Environ(), extraEnv...)
		}
		if out, err := cmd.CombinedOutput(); err != nil {
			return fmt.Errorf("build %s@%s: %w\n%s", target, version, err, out)
		}
		return nil
	}

	if err := build("./cmd/banger", filepath.Join(stage, "banger")); err != nil {
		return err
	}

	if strings.HasPrefix(version, "v0.smoke.broken-") {
		const brokenScript = `#!/bin/sh
case "$*" in
  *--check-migrations*)
    printf 'compatible: smoke broken-bangerd pretends to be ready\n'
    exit 0
    ;;
  *)
    printf 'smoke broken-bangerd: refusing to run as daemon\n' >&2
    exit 1
    ;;
esac
`
		if err := os.WriteFile(filepath.Join(stage, "bangerd"), []byte(brokenScript), 0o755); err != nil {
			return fmt.Errorf("write broken bangerd: %w", err)
		}
	} else {
		if err := build("./cmd/bangerd", filepath.Join(stage, "bangerd")); err != nil {
			return err
		}
	}

	if err := build("./cmd/banger-vsock-agent", filepath.Join(stage, "banger-vsock-agent"),
		"CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64"); err != nil {
		return err
	}

	tarballName := fmt.Sprintf("banger-%s-linux-amd64.tar.gz", version)
	tarballPath := filepath.Join(outDir, tarballName)
	if err := writeTarGz(stage, tarballPath); err != nil {
		return fmt.Errorf("tar %s: %w", version, err)
	}

	body, err := os.ReadFile(tarballPath)
	if err != nil {
		return fmt.Errorf("read tarball: %w", err)
	}
	hash := sha256.Sum256(body)
	sumsBody := fmt.Sprintf("%x  %s\n", hash, tarballName)
	if err := os.WriteFile(filepath.Join(outDir, "SHA256SUMS"), []byte(sumsBody), 0o644); err != nil {
		return fmt.Errorf("write SHA256SUMS: %w", err)
	}

	sig, err := signCosignBlob(smokeRelKey, []byte(sumsBody))
	if err != nil {
		return fmt.Errorf("sign SHA256SUMS for %s: %w", version, err)
	}
	if err := os.WriteFile(filepath.Join(outDir, "SHA256SUMS.sig"), []byte(sig), 0o644); err != nil {
		return fmt.Errorf("write sig: %w", err)
	}

	return os.RemoveAll(stage)
}

// signCosignBlob produces a cosign-compatible blob signature: ASN.1 DER
// ECDSA over SHA256(body), base64 encoded with no newline. This is the
// exact wire format cosign produces and the Go updater verifies, and
// matches the bash chain `openssl dgst -sha256 -sign | base64 -w0`.
func signCosignBlob(priv *ecdsa.PrivateKey, body []byte) (string, error) {
	hash := sha256.Sum256(body)
	sig, err := ecdsa.SignASN1(rand.Reader, priv, hash[:])
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(sig), nil
}

// writeTarGz packages every regular file in srcDir at the root of a
// gzip tarball at dst. Mirrors the bash `tar czf` of the staged binary
// trio (banger, bangerd, banger-vsock-agent).
func writeTarGz(srcDir, dst string) error {
	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()
	gw := gzip.NewWriter(out)
	defer gw.Close()
	tw := tar.NewWriter(gw)
	defer tw.Close()

	entries, err := os.ReadDir(srcDir)
	if err != nil {
		return err
	}
	for _, e := range entries {
		if !e.Type().IsRegular() {
			continue
		}
		path := filepath.Join(srcDir, e.Name())
		st, err := os.Stat(path)
		if err != nil {
			return err
		}
		hdr := &tar.Header{
			Name:    e.Name(),
			Mode:    int64(st.Mode().Perm()),
			Size:    st.Size(),
			ModTime: st.ModTime(),
		}
		if err := tw.WriteHeader(hdr); err != nil {
			return err
		}
		f, err := os.Open(path)
		if err != nil {
			return err
		}
		if _, err := io.Copy(tw, f); err != nil {
			f.Close()
			return err
		}
		f.Close()
	}
	return nil
}

func writeSmokeManifest(path, base string) error {
	body := fmt.Sprintf(`{
  "schema_version": 1,
  "latest_stable": %q,
  "releases": [
    {
      "version": %q,
      "tarball_url": "%s/%s/banger-%s-linux-amd64.tar.gz",
      "sha256sums_url": "%s/%s/SHA256SUMS",
      "sha256sums_sig_url": "%s/%s/SHA256SUMS.sig",
      "released_at": "2026-04-29T00:00:00Z"
    },
    {
      "version": %q,
      "tarball_url": "%s/%s/banger-%s-linux-amd64.tar.gz",
      "sha256sums_url": "%s/%s/SHA256SUMS",
      "sha256sums_sig_url": "%s/%s/SHA256SUMS.sig",
      "released_at": "2026-04-30T00:00:00Z"
    }
  ]
}
`,
		smokeReleaseGood,
		smokeReleaseGood,
		base, smokeReleaseGood, smokeReleaseGood,
		base, smokeReleaseGood,
		base, smokeReleaseGood,
		smokeReleaseBroken,
		base, smokeReleaseBroken, smokeReleaseBroken,
		base, smokeReleaseBroken,
		base, smokeReleaseBroken,
	)
	return os.WriteFile(path, []byte(body), 0o644)
}

// repoRoot resolves the repo root (where go.mod lives) from the test
// binary's cwd. `go test` runs each package's tests from that package's
// source dir, so internal/smoketest -> ../.. lands at the root.
func repoRoot() (string, error) {
	cwd, err := os.Getwd()
	if err != nil {
		return "", err
	}
	return filepath.Abs(filepath.Join(cwd, "..", ".."))
}
