// 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.

package fips140

import (
	"crypto/sha256"
	"flag"
	"fmt"
	"internal/testenv"
	"maps"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"
)

var update = flag.Bool("update", false, "update GOROOT/lib/fips140/fips140.sum")

func TestSums(t *testing.T) {
	lib := filepath.Join(testenv.GOROOT(t), "lib/fips140")
	file := filepath.Join(lib, "fips140.sum")
	sums, err := os.ReadFile(file)
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.SplitAfter(string(sums), "\n")

	zips, err := filepath.Glob(filepath.Join(lib, "*.zip"))
	if err != nil {
		t.Fatal(err)
	}

	format := func(name string, sum [32]byte) string {
		return fmt.Sprintf("%s %x\n", name, sum[:])
	}

	want := make(map[string]string)
	for _, zip := range zips {
		data, err := os.ReadFile(zip)
		if err != nil {
			t.Fatal(err)
		}
		name := filepath.Base(zip)
		want[name] = format(name, sha256.Sum256(data))
	}

	// Process diff, deleting or correcting stale lines.
	var diff []string
	have := make(map[string]bool)
	for i, line := range lines {
		if line == "" {
			continue
		}
		if strings.HasPrefix(line, "#") || line == "\n" {
			// comment, preserve
			diff = append(diff, " "+line)
			continue
		}
		name, _, _ := strings.Cut(line, " ")
		if want[name] == "" {
			lines[i] = ""
			diff = append(diff, "-"+line)
			continue
		}
		have[name] = true
		fixed := want[name]
		delete(want, name)
		if line == fixed {
			diff = append(diff, " "+line)
		} else {
			// zip hashes should never change once listed
			t.Errorf("policy violation: zip file hash is changing:\n-%s+%s", line, fixed)
			lines[i] = fixed
			diff = append(diff, "-"+line, "+"+fixed)
		}
	}

	// Add missing lines.
	// Sort keys to avoid non-determinism, but overall file is not sorted.
	// It will end up time-ordered instead.
	for _, name := range slices.Sorted(maps.Keys(want)) {
		line := want[name]
		lines = append(lines, line)
		diff = append(diff, "+"+line)
	}

	// Show diffs or update file.
	fixed := strings.Join(lines, "")
	if fixed != string(sums) {
		if *update && !t.Failed() {
			t.Logf("updating GOROOT/lib/fips140/fips140.sum:\n%s", strings.Join(diff, ""))
			if err := os.WriteFile(file, []byte(fixed), 0666); err != nil {
				t.Fatal(err)
			}
			return
		}
		t.Errorf("GOROOT/lib/fips140/fips140.sum out of date. changes needed:\n%s", strings.Join(diff, ""))
	}
}

func TestVerifyZipSum(t *testing.T) {
	dir := t.TempDir()
	zipfile := filepath.Join(dir, "v1.2.3.zip")
	data := []byte("not really a zip, but it does not matter here")
	if err := os.WriteFile(zipfile, data, 0666); err != nil {
		t.Fatal(err)
	}
	sum := sha256.Sum256(data)

	sumfile := filepath.Join(dir, "fips140.sum")
	write := func(contents string) {
		if err := os.WriteFile(sumfile, []byte(contents), 0666); err != nil {
			t.Fatal(err)
		}
	}

	// Matching hash with comments and a second entry passes.
	write(fmt.Sprintf("# comment\n\nv1.2.3.zip %x\nother.zip 0000000000000000000000000000000000000000000000000000000000000000\n", sum[:]))
	if err := verifyZipSum(zipfile, sumfile); err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	// Missing entry for this zip fails.
	write("# only comments\n")
	if err := verifyZipSum(zipfile, sumfile); err == nil {
		t.Errorf("expected error when hash entry is missing")
	}

	// Wrong hash fails.
	write("v1.2.3.zip 0000000000000000000000000000000000000000000000000000000000000000\n")
	if err := verifyZipSum(zipfile, sumfile); err == nil {
		t.Errorf("expected error when hash does not match")
	}

	// Missing sum file fails.
	if err := verifyZipSum(zipfile, filepath.Join(dir, "does-not-exist.sum")); err == nil {
		t.Errorf("expected error when sum file is missing")
	}
}
