// Copyright (c) 2026 Tigera, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cilanes

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// nonSlug matches every run of characters a set name leaves out.
var nonSlug = regexp.MustCompile(`[^a-z0-9]+`)

// ResolveSets maps every lane that runs the e2e binary to its set name. Two
// lanes claiming one name would overwrite each other's set, so that is an error.
func ResolveSets(lanes []Lane) (map[string]Lane, error) {
	sets := map[string]Lane{}
	for _, l := range lanes {
		if !l.RunsE2EBinary() {
			// run_tests.sh exits 1 on an e2e lane that selects nothing.
			if l.TestType == "k8s-e2e" && l.Area != "" {
				return nil, fmt.Errorf("%s (%s) runs the e2e binary but selects nothing", l.Name, l.Source)
			}
			continue
		}
		name, err := SetName(l)
		if err != nil {
			return nil, fmt.Errorf("%s (%s): %w", l.Name, l.Source, err)
		}
		if other, ok := sets[name]; ok {
			return nil, fmt.Errorf("%s (%s) and %s (%s) both resolve to set %s", other.Name, other.Source, l.Name, l.Source, name)
		}
		sets[name] = l
	}
	return sets, nil
}

// SetName names the lane's set after the lane itself: the sets/ subdirectory of
// the lane's source directory, the file that declares it, and the lane name
// reduced to lowercase words joined by hyphens. It fails for a source outside
// the known lane directories and for a name with nothing left to slug.
func SetName(l Lane) (string, error) {
	dir := ""
	for _, src := range laneSources {
		if src.dir == filepath.Dir(l.Source) {
			dir = src.setDir
		}
	}
	if dir == "" {
		return "", fmt.Errorf("source %q is not a known lane directory", l.Source)
	}
	file := strings.TrimSuffix(filepath.Base(l.Source), filepath.Ext(l.Source))
	lane := strings.Trim(nonSlug.ReplaceAllString(strings.ToLower(l.Name), "-"), "-")
	if lane == "" {
		return "", fmt.Errorf("lane name %q leaves nothing to name a set by", l.Name)
	}
	return dir + "/" + file + "/" + lane, nil
}

// Write records the index and one file per set under dir.
func Write(dir string, lanes []Lane, specs map[string][]string) error {
	setsDir := filepath.Join(dir, "sets")
	if err := os.RemoveAll(setsDir); err != nil {
		return err
	}
	for name, list := range specs {
		path := filepath.Join(setsDir, filepath.FromSlash(name)+".txt")
		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
			return err
		}
		var body string
		if len(list) > 0 {
			body = strings.Join(list, "\n") + "\n"
		}
		if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
			return err
		}
	}

	var index strings.Builder
	index.WriteString("# Generated by `make gen-test-set`. Do not edit.\n")
	index.WriteString("# ci file | lane | test set | specs\n")
	for _, l := range lanes {
		set, count := "("+l.TestType+")", "-"
		if l.Area == "" {
			set = "(no tests)"
		}
		if l.RunsE2EBinary() {
			name, err := SetName(l)
			if err != nil {
				return err
			}
			set, count = name, fmt.Sprint(len(specs[name]))
		}
		fmt.Fprintf(&index, "%s | %s | %s | %s\n", l.Source, l.Name, set, count)
	}
	return os.WriteFile(filepath.Join(dir, "index.txt"), []byte(index.String()), 0o644)
}
