package infra

import (
	"go/ast"
	"go/parser"
	"go/token"
	"strconv"
	"strings"
	"testing"
)

// TestImagesCoversEveryPin is the anti-drift gate for the golden bake.
//
// Images() feeds `felhom-controller --print-infra-images`, which build-golden.sh uses to decide what
// to pre-pull into the appliance image. If someone adds a fifth infra stack — a new `FooImage` const
// — and forgets to add it to Images(), the golden silently bakes 4 of 5 and enabling that stack on a
// fresh box goes back to being a multi-minute silent registry pull. That is exactly how felhom-samba
// was missed, so this test reads the CONST BLOCK OUT OF THE SOURCE rather than restating the list:
// a hand-written expected list would need the same edit and would rot the same way.
func TestImagesCoversEveryPin(t *testing.T) {
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "infra.go", nil, 0)
	if err != nil {
		t.Fatalf("parsing infra.go: %v", err)
	}

	pins := map[string]string{} // const name -> image ref
	for _, decl := range f.Decls {
		gd, ok := decl.(*ast.GenDecl)
		if !ok || gd.Tok != token.CONST {
			continue
		}
		for _, spec := range gd.Specs {
			vs, ok := spec.(*ast.ValueSpec)
			if !ok || len(vs.Names) != 1 || len(vs.Values) != 1 {
				continue
			}
			name := vs.Names[0].Name
			if !strings.HasSuffix(name, "Image") {
				continue
			}
			lit, ok := vs.Values[0].(*ast.BasicLit)
			if !ok || lit.Kind != token.STRING {
				continue
			}
			val, err := strconv.Unquote(lit.Value)
			if err != nil {
				t.Fatalf("unquoting %s: %v", name, err)
			}
			pins[name] = val
		}
	}

	if len(pins) == 0 {
		t.Fatal("found no *Image consts in infra.go — the parser walk is broken, not the pins")
	}

	got := map[string]bool{}
	for _, img := range Images() {
		got[img] = true
	}
	for name, img := range pins {
		if !got[img] {
			t.Errorf("const %s = %q is not in Images() — the golden bake would not pre-pull it, so "+
				"enabling that stack on a fresh box would block on a silent registry pull", name, img)
		}
	}
	if len(Images()) != len(pins) {
		t.Errorf("Images() has %d entries but infra.go declares %d *Image consts (%v) — they must "+
			"correspond one-to-one", len(Images()), len(pins), pins)
	}
}

// TestImagesArePinned guards the other half of the contract: a floating tag would make the golden
// bake unreproducible (the bake and a later deploy could resolve the same name to different digests).
func TestImagesArePinned(t *testing.T) {
	for _, img := range Images() {
		if !strings.Contains(img, ":") {
			t.Errorf("infra image %q has no tag — implicitly :latest", img)
			continue
		}
		if strings.HasSuffix(img, ":latest") {
			t.Errorf("infra image %q is :latest — a floating tag breaks reproducible golden bakes", img)
		}
	}
}
