// Package main writes HTML files for each Go source file listed in the user-specified Go coverage profile file.
//
// This module also generates a directory tree HTML file rendered within an iframe of the index HTML file.
//
// The header portion of the index HTML file will also render two buttons if the browser's CORS policies allow it. These buttons are:
//
//   "theme"                  - toggles between two hardcoded "light" and "dark" themes
//   "expand" (or "collapse") - toggles the opening (or closing) of all subdirectories rendered within the tree HTML document
//
// Note that the "theme" and "expand" / "collapse" buttons will not be rendered when the index page is loaded via the file:// scheme.
//
// A simple workaround is to instantiate an HTTP server to serve the HTML files, e.g.:
//
//   $ python3 -m http.server 8000
//
// and then load http://localhost:8000/ in a browser.
package main

import (
	"bufio"
	"bytes"
	"cmp"
	"context"
	"embed"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"maps"
	"math"
	"os"
	"path/filepath"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"text/template"

	"github.com/graniticio/inifile"
	"github.com/jbunds/coverage/progress"
	"golang.org/x/mod/modfile"
	"golang.org/x/sync/errgroup"
	"golang.org/x/term"
	"golang.org/x/tools/cover"
	"golang.org/x/tools/go/packages"
)

//go:embed html/* css/* img/*
var embeddedFiles embed.FS

var (
	isTerm         = false
	styleCSS       = "css/style.css"   // embedded template (.MaxWidth)
	indexHTML      = "html/index.html" // embedded template (.ModName)
	treeHTML       = "tree.html"       // generated by tree.go
	ancillaryFiles = []string{
		"css/tree.css",
		"img/favicon.ico",
		"img/go-blue-gradient.svg",
	}
)

func init() {
	fd := os.Stderr.Fd()
	if fd <= math.MaxInt && term.IsTerminal(int(fd)) {
		isTerm = true
	}
}

const (
	colorReset = "\033[0m"
	colorRed   = "\033[31m"
	colorGreen = "\033[32m"
)

// stickyWriter is a wrapper interface used to enable sequential string writing with deferred error handling
type stickyWriter interface {
	io.Writer
	write(string)
	err() error
}

// errorWriter tracks the first encountered I/O failure to allow multiple sequential writes without repetitive inline error checking
type errorWriter struct {
	w io.Writer
	e error
}

// write performs a sticky-error write
func (e *errorWriter) write(s string) {
	if e.e != nil { return }
	_, e.e = io.WriteString(e.w, s)
}

// write performs a sticky-error write that optionally wraps the provided string in ANSI color codes
func (e *errorWriter) writeColor(s, color string) {
	if e.e != nil { return }
	if isTerm { _, e.e = io.WriteString(e.w, color     ) }
	            _, e.e = io.WriteString(e.w, s         )
	if isTerm { _, e.e = io.WriteString(e.w, colorReset) }
}

// err returns the first I/O failure encountered during multiple sequential write operations
func (e *errorWriter) err() error { return e.e }

// Write satisfies the io.Writer interface, but it unused
func (e *errorWriter) Write(p []byte) (int, error) {
	if e.e != nil { return 0, e.e }
	n := 0
	n, e.e = e.w.Write(p)
	return n, e.e
}

// pkgDirCache maps canonical Go packages to their respective paths on disk
type pkgDirCache struct {
	cache map[string]string
	mu    sync.RWMutex
}

// coverage tracks per-file coverage
type coverage struct {
	covered int64
	total   int64
}

// workUnit represents a unit of work to be performed: the generation of an HTML file for a Go source file's coverage profile
// the main program utilizes all available CPU cores to process all workUnits concurrently
type workUnit struct {
	profile *cover.Profile
	outPath string
}

// reportGenerator orchestrates the generation of the HTML report
// it stores state, simplifies method signatures, avoids copying values, and makes testing easier
type reportGenerator struct {
	fsys            writeFS
	embeddedFiles   fs.FS
	modFile         string
	modName         string
	repoURL         string
	pkgDirCache     *pkgDirCache
	outRoot         string
	profilePath     string
	profiles        []*cover.Profile
	cov             map[string]coverage
	totalCovered    atomic.Int64
	totalStatements atomic.Int64
	maxWidth        int
	ancillaryFiles  []string
}

// wrappers to facilitate test injection

// writeFS defines an interface that extends fs.FS with writing capabilities.
// This abstraction is necessary to allow for mocking the file system within
// unit tests, as the standard "os" package cannot be directly mocked.
type writeFS interface {
	fs.FS
	Create         (context.Context, string)                      (io.WriteCloser, error)
	MkdirAll       (context.Context, string,         fs.FileMode)                  error
	ReadFile       (context.Context, string)                      ([]byte,         error)
	WriteFile      (context.Context, string, []byte, fs.FileMode)                  error
	OpenWithContext(context.Context, string)                      (fs.File,        error)
}

// localFS provides a concrete implementation of the writeFS interface by
// wrapping the standard "os" package. This allows the program to perform
// actual system operations in production while remaining easily testable
// via alternative interface implementations.
type localFS struct{}

func (lfs *localFS) Open(name string) (fs.File, error) {
	return os.Open(filepath.Clean(name))
}

func (lfs *localFS) Create(ctx context.Context, name string) (io.WriteCloser, error) {
	if err := ctx.Err(); err != nil { return nil, err }
	return os.Create(filepath.Clean(name))
}

func (lfs *localFS) MkdirAll(ctx context.Context, path string, perm fs.FileMode) error {
	if err := ctx.Err(); err != nil { return err }
	return os.MkdirAll(filepath.Clean(path), perm)
}

func (lfs *localFS) ReadFile(ctx context.Context, name string) ([]byte, error) {
	if err := ctx.Err(); err != nil { return nil, err }
	return fs.ReadFile(lfs, name)
}

func (lfs *localFS) WriteFile(ctx context.Context, name string, data []byte, perm fs.FileMode) error {
	if err := ctx.Err(); err != nil { return err }
	return os.WriteFile(filepath.Clean(name), data, perm)
}

func (lfs *localFS) OpenWithContext(ctx context.Context, name string) (fs.File, error) {
	if err := ctx.Err(); err != nil { return nil, err }
	return lfs.Open(filepath.Clean(name))
}

// wraps inifile.IniConfig.Value for test injection
type iniFileValueGetter interface { Value(string, string) (string, error) }

// wraps packages.Load for test injection
type pkgLoader func(cfg *packages.Config, patterns ...string) ([]*packages.Package, error)

func main() {
	goModFile, profilePath, outRoot, err := flags(flag.CommandLine, filterArgs(os.Args[1:]), os.Stderr)
	if err != nil {
		fmt.Fprintf(os.Stderr, "cannot parse flags: %v\n", err)
		os.Exit(1)
	}

	profiles, err := cover.ParseProfiles(profilePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "cannot parse coverage profile file: %v\n", err)
		os.Exit(2)
	}

	fmt.Fprintf(os.Stderr, "processing %d source files...", len(profiles))
	if !isTerm { fmt.Println() }

	ctx    := context.Background()
	repGen := &reportGenerator{
		fsys:           &localFS{},
		embeddedFiles:  embeddedFiles,
		modFile:        goModFile,
		outRoot:        filepath.Clean(outRoot),
		profilePath:    filepath.Clean(profilePath),
		profiles:       profiles,
		ancillaryFiles: ancillaryFiles,
	}

	if err := repGen.getModName(ctx, goModFile); err != nil { // sets repGen.modName
		fmt.Fprintf(os.Stderr, "cannot determine module name: %v\n", err)
		os.Exit(3)
	}

	var gitConfig *inifile.IniConfig
	gitConfigPath := filepath.Join(filepath.Dir(goModFile), ".git", "config")
	if gitConfig, err = inifile.NewIniConfigFromPath(gitConfigPath); err != nil {
		fmt.Fprintf(os.Stderr, "cannot parse Git config file (%q): %v\n", gitConfigPath, err)
		os.Exit(4)
	}

	if err := repGen.getRepoURL(ctx, gitConfig); err != nil { // sets repGen.repoURL
		fmt.Fprintf(os.Stderr, "cannot determine repo URL: %v\n", err)
		os.Exit(5)
	}

	if err := repGen.primePkgDirCache(ctx, packages.Load, profilePath); err != nil { // sets repGen.pkgDirCache
		fmt.Fprintf(os.Stderr, "cannot prime package directory cache: %v\n", err)
		os.Exit(6)
	}

	if err := repGen.writeCovHTMLFiles(ctx, os.Stderr); err != nil { // sets repGen.cov
		fmt.Fprintf(os.Stderr, "cannot write HTML coverage files: %v\n", err)
		os.Exit(7)
	}

	if err := repGen.writeIndexHTML(ctx, indexHTML); err != nil { // requires repGen.modName
		fmt.Fprintf(os.Stderr, "cannot write %q: %v\n", indexHTML, err)
		os.Exit(8)
	}

	tb := &treeBuilder{
		fsys:    &localFS{},
		outRoot: filepath.Clean(outRoot),
		cov:     repGen.cov,
	}

	if repGen.maxWidth, err = tb.writeTreeHTML(ctx, os.Stderr); err != nil {
		fmt.Fprintf(os.Stderr, "cannot write %q: %v\n", treeHTML, err)
		os.Exit(9)
	}

	if err := repGen.writeStyleCSS(ctx, styleCSS); err != nil { // requires repGen.maxWidth
		fmt.Fprintf(os.Stderr, "cannot write %s: %v\n", styleCSS, err)
		os.Exit(10)
	}

	if err := repGen.writeAncillaryFiles(ctx); err != nil {
		fmt.Fprintf(os.Stderr, "cannot write ancillary files: %v\n", err)
		os.Exit(11)
	}

	if err := repGen.printCoverage(ctx, os.Stdout); err != nil { // requires repGen.cov
		fmt.Fprintf(os.Stderr, "cannot print per-file coverage figures: %v\n", err)
		os.Exit(12)
	}
}

// getModName reads the repo's root go.mod file to determine the name of the Go module
func (rg *reportGenerator) getModName(ctx context.Context, goModFile string) error {
	if err := ctx.Err(); err != nil { return err }

	goMod, err := rg.fsys.ReadFile(ctx, goModFile)
	if err != nil                          { return fmt.Errorf("cannot read %q: %w",  goModFile, err) }
	modFile, err := modfile.Parse(goModFile, goMod, nil)
	if err != nil || modFile.Module == nil { return fmt.Errorf("cannot parse %q: %w", goModFile, err) }
	rg.modName = modFile.Module.Mod.Path

	return nil
}

// getRepoURL converts a Git remote URL to an HTTP URL for subsequent use in writeIndexHTML
func (rg *reportGenerator) getRepoURL(ctx context.Context, gitConfig iniFileValueGetter) error {
	if err := ctx.Err(); err != nil { return err }

	gitRemoteURL, err := gitConfig.Value(`remote "origin"`, "url")
	if err != nil { return err }

	httpURL := gitRemoteURL
	httpURL  = strings.TrimPrefix(httpURL, "ssh://")
	httpURL  = strings.TrimPrefix(httpURL, "git://")
	if idx  := strings.Index     (httpURL, "@"); idx != -1 { httpURL = httpURL[idx+1:] }
	httpURL  = strings.Replace   (httpURL, ":", "/", 1)
	httpURL  = strings.TrimSuffix(httpURL, ".git")
	httpURL  = strings.TrimSuffix(httpURL, "/")

	if !strings.HasPrefix(httpURL, "http") { httpURL = "https://" + httpURL }
	rg.repoURL = httpURL

	return nil
}

// getAllPkgPaths extracts all unique package paths from the coverage profile file for subsequent use in primePkgDirCache
func (rg *reportGenerator) getAllPkgPaths(ctx context.Context, profilePath string) ([]string, error) {
	if err := ctx.Err(); err != nil { return nil, err }

	f, err := rg.fsys.OpenWithContext(ctx, filepath.Clean(profilePath))
	if err != nil { return nil, err }

	defer func() {
		closeErr := f.Close()
		if err == nil { err = closeErr }
	}()

	pkgSet  := make(map[string]struct{})
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "mode:") { continue }

		parts := strings.Split(line, ":")
		if len(parts) < 2 { continue }

		pkgSet[filepath.Dir(parts[0])] = struct{}{}
	}

	allPaths := make([]string, 0, len(pkgSet))
	for p := range pkgSet { allPaths = append(allPaths, p) }

	return allPaths, scanner.Err()
}

// primePkgDirCache primes rg.pkgDirCache for subsequent use in buildCovHTML
func (rg *reportGenerator) primePkgDirCache(ctx context.Context, pkgLoader pkgLoader, profilePath string) error {
	if err := ctx.Err(); err != nil { return err }

	rg.pkgDirCache    = &pkgDirCache{ cache: make(map[string]string) }
	allPkgPaths, err := rg.getAllPkgPaths(ctx, profilePath)
	if err != nil { return err }

	cfg := &packages.Config{
		Mode:  packages.NeedFiles | packages.NeedModule | packages.NeedName,
		Tests: false,
		Dir:   filepath.Dir(rg.modFile),
	}
	pkgs, err := pkgLoader(cfg, allPkgPaths...)
	if err != nil { return err }

	rg.pkgDirCache.mu.Lock()
	defer rg.pkgDirCache.mu.Unlock()

	for _, pkg := range pkgs {
		if len(pkg.GoFiles) > 0 {
			rg.pkgDirCache.cache[pkg.PkgPath] = filepath.Dir(pkg.GoFiles[0])
		}
	}

	return nil
}

// writeCovHTMLFiles calculates per-file coverage percentages and writes a *.go.html file for each Go source file listed in the coverage profile file
func (rg *reportGenerator) writeCovHTMLFiles(ctx context.Context, progressOutput io.Writer) error {
	if err := ctx.Err(); err != nil { return err }

	rg.cov        = make(map[string]coverage, len(rg.profiles))
	units        := make([]workUnit, 0, len(rg.profiles))
	dirsToCreate := make(map[string]struct{}) // exclude duplicate directories and keep the relatively heavy mkdir syscalls outside of the concurrent loop

	for _, profile := range rg.profiles {
		outPath := filepath.Clean(filepath.Join(rg.outRoot, profile.FileName + ".html"))
		units    = append(units, workUnit{
			profile: profile,
			outPath: outPath,
		})
		dirsToCreate[filepath.Dir(outPath)] = struct{}{}
	}

	group, gCtx := errgroup.WithContext(ctx)
	progDirs    := progress.NewProgress(uint64(len(dirsToCreate)), progressOutput)
	defer progDirs.Close()

	for dir := range dirsToCreate {
		group.Go(func() error {
			if err := rg.fsys.MkdirAll(gCtx, dir, 0700); err != nil {
				return fmt.Errorf("cannot create directory %q: %w", dir, err)
			}
			progDirs.Report(1, "created " + dir)
			return nil
		})
	}

	if err := group.Wait(); err != nil { return err }

	progDirs.Close()
	
	// The total number of statements (cover.ProfileBlock.NumStmt) is used as a normalizing
	// proxy for work units to ensure the progress tracker accurately reflects the
	// computational effort required for files of vastly different sizes, e.g.:
	//
	//   k8s.io/kubernetes/pkg/kubelet/apis/config/register.go // contains    1 func def
	//   k8s.io/api/core/v1/generated.pb.go                    // contains 1692 func defs
	//
	// Since the global total of statements is not known a priori, dynamic discovery is
	// implemented whereby each worker goroutine calls AddTotal() as it parses a source
	// file's profiled blocks, allowing the progress tracker to refine its denominator
	// in real-time without requiring a separate pre-processing pass.

	perFileCov := make([]coverage, len(units))

	progFiles := progress.NewProgress(0, progressOutput)
	defer progFiles.Close()

	group, gCtx = errgroup.WithContext(ctx)
	group.SetLimit(runtime.NumCPU()) // full send

	for i, unit := range units {
		group.Go(func() error {
			if err := gCtx.Err(); err != nil { return err }

			var fileStatements, fileCovered int64
			for _, block := range unit.profile.Blocks {
				fileStatements += int64(block.NumStmt)
				if block.Count > 0 {
					fileCovered += int64(block.NumStmt)
				}
			}

			progFiles.AddTotal(uint64(fileStatements))

			var buf bytes.Buffer
			ew := &errorWriter{w: &buf}
			if err := rg.buildCovHTML(ctx, ew, unit.profile, unit.profile.FileName); err != nil {
				return fmt.Errorf("cannot build HTML for %q: %w", unit.profile.FileName, err)
			}

			if err := rg.fsys.WriteFile(ctx, unit.outPath, buf.Bytes(), 0600); err != nil {
				return fmt.Errorf("cannot write HTML file for %q: %w", unit.profile.FileName, err)
			}

			progFiles.Report(float64(fileStatements), unit.profile.FileName)

			rg.totalCovered.Add(fileCovered)
			rg.totalStatements.Add(fileStatements)

			perFileCov[i] = coverage{
				covered: fileCovered,
				total:   fileStatements,
			}

			return nil
		})
	}

	err := group.Wait()

	progFiles.Close()

	for i, cov := range perFileCov {
		rg.cov[units[i].profile.FileName] = cov
	}

	return err
}

// buildCovHTML builds the HTML content for a single *.go.html file, with green (covered) and red (uncovered) lines to indicate test coverage
func (rg *reportGenerator) buildCovHTML(ctx context.Context, ew stickyWriter, profile *cover.Profile, srcPath string) error {
	if err := ctx.Err(); err != nil { return err }

	pkgPath  := filepath.Dir (profile.FileName)
	fileName := filepath.Base(profile.FileName)

	src, err := rg.fsys.ReadFile(ctx, filepath.Join(rg.pkgDirCache.cache[pkgPath], fileName))
	if err != nil { return err }

	cssPath := strings.Repeat("../", strings.Count(srcPath, "/")) + filepath.Base(styleCSS)
	writePreamble(ew, cssPath, srcPath)

	pos := 0
	for _, b := range profile.Boundaries(src) {
		chunk := src[pos:b.Offset]
		if b.Start {
			class := "miss"
			if b.Count > 0 { class = "hit" }
			if nl := bytes.LastIndexByte(chunk, '\n'); nl != -1 {
				template.HTMLEscape(ew, chunk[:nl + 1])
				ew.write(`<span class="`)
				ew.write(class)
				ew.write(`">`)
				template.HTMLEscape(ew, chunk[nl + 1:])
			} else {
				template.HTMLEscape(ew, chunk)
				ew.write(`<span class="`)
				ew.write(class)
				ew.write(`">`)
			}
		} else {
			template.HTMLEscape(ew, chunk)
			ew.write("</span>")
		}
		pos = b.Offset
	}

	template.HTMLEscape(ew, src[pos:])
	writePostamble(ew)
	return ew.err()
}

// writePreamble writes the preamble portion of the HTML content common to every Go source HTML file
func writePreamble(ew stickyWriter, cssRelPath, srcPath string) {
	ew.write("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n")
	ew.write("<link rel=\"stylesheet\" href=\"")
	ew.write(cssRelPath)
	ew.write("\" type=\"text/css\">\n<title>")
	ew.write(srcPath)
	ew.write("</title>\n</head>\n<body id=\"code\">\n<pre>\n")
}

// writePostamble writes the postamble portion of the HTML content common to every Go source HTML file
func writePostamble(ew stickyWriter) {
	ew.write("</pre>\n<script>")
	ew.write(`
try {
  const parentTheme = window.parent.document.documentElement.getAttribute('theme');
  if (parentTheme) document.documentElement.setAttribute('theme', parentTheme);
} catch (e) {
  console.warn('direct parent access blocked by browser; waiting for postMessage');
}

window.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SET_THEME') document.documentElement.setAttribute('theme', event.data.theme);
});
</script>
</body>
</html>`)
}

// printCoverage prints per-file coverage percentages to the specified destination (typically stdout)
func (rg *reportGenerator) printCoverage(ctx context.Context, w io.Writer) error {
	if err := ctx.Err(); err != nil { return err }

	keys       := slices.Collect(maps.Keys(rg.cov))
	maxPathLen := len(slices.MaxFunc(keys, func(a, b string) int {
		return cmp.Compare(len(a), len(b))
	}))

	maxPathLen = max(maxPathLen, 5) // 5 == len("Total")

	// TODO(jeff): allow users to chose how the rows rendered in the tree should be sorted;
	//             default should probably path-depth, then alphanumerically, just like here
	slices.SortFunc(keys, func(a, b string) int {
		depthA, depthB := strings.Count(a, "/"), strings.Count(b, "/")
		if depthA != depthB { return cmp.Compare(depthA, depthB) } // sort by path depth
		return cmp.Compare(a, b) // sort alphanumerically
	})

	divider := strings.Repeat("—", maxPathLen + 9) + "\n" // 9 == 2 spaces + len("100.00%")

	ew := &errorWriter{w: w}
	ew.write("File")
	ew.write(strings.Repeat(" ", maxPathLen - 4 + 1))
	ew.write("Coverage\n")
	ew.write(divider)

	for _, path := range keys {
		cov     := rg.cov[path]
		percent := 0.0
		if cov.total > 0 {
			percent = float64(cov.covered) / float64(cov.total) * 100
		}
		ew.write(path)
		ew.write(strings.Repeat(" ", maxPathLen - len(path) + 2))
		pct       := strconv.FormatFloat(percent, 'f', 2, 64)
		colorCode := colorGreen
		if percent < 50 { colorCode = colorRed }
		ew.write(strings.Repeat(" ", 6 - len(pct)))
		ew.writeColor(pct + "%", colorCode)
		ew.write("\n")
	}

	totalPercent    := 0.0
	totalCovered    := rg.totalCovered.Load()
	totalStatements := rg.totalStatements.Load()
	if totalStatements > 0 {
		totalPercent = float64(totalCovered) / float64(totalStatements) * 100
	}
	ew.write(divider)
	ew.write("Total")
	ew.write(strings.Repeat(" ", maxPathLen - 5 + 2))
	totalPct  := strconv.FormatFloat(totalPercent, 'f', 2, 64)
	colorCode := colorGreen
	if totalPercent < 50 { colorCode = colorRed }
	ew.write(strings.Repeat(" ", 6 - len(totalPct)))
	ew.writeColor(totalPct + "%", colorCode)
	ew.write("\n")

	return ew.err()
}

// writeIndexHTML writes the index HTML file, which contains two template parameters
// (ModName and ModURL), and hosts two iframes (directory tree & source code)
func (rg *reportGenerator) writeIndexHTML(ctx context.Context, indexHTML string) error {
	if err := ctx.Err(); err != nil { return err }

	data := struct{
		ModName, ModURL string
	}{
		ModName: rg.modName,
		ModURL:  rg.repoURL,
	}

	return rg.writeTemplateFile(ctx, indexHTML, data)
}

// writeStyleCSS writes the style.css file, which contains a single "MaxWidth" template parameter
func (rg *reportGenerator) writeStyleCSS(ctx context.Context, styleCSS string) error {
	if err := ctx.Err(); err != nil { return err }

	data := struct{
		MaxWidth int
	}{
		MaxWidth: rg.maxWidth,
	}

	return rg.writeTemplateFile(ctx, styleCSS, data)
}

// writeTemplateFile writes the specified template file
func (rg *reportGenerator) writeTemplateFile(ctx context.Context, file string, tmplVars any) error {
	if err := ctx.Err(); err != nil { return err }

	outFile   := filepath.Clean(filepath.Join(rg.outRoot, filepath.Base(file)))
	tmpl, err := template.ParseFS(rg.embeddedFiles, file)
	if err != nil                                   { return fmt.Errorf("cannot parse %q: %w",     file, err) }
	f, err := rg.fsys.Create(ctx, outFile)
	if err != nil                                   { return fmt.Errorf("cannot create %q: %w", outFile, err) }
	if err := tmpl.Execute(f, tmplVars); err != nil { return fmt.Errorf("cannot render template: %w",    err) }

	return f.Close()
}

// writeAncillaryFiles writes the files required by the coverage report to the user-specified path
func (rg *reportGenerator) writeAncillaryFiles(ctx context.Context) error {
	if err := ctx.Err(); err != nil { return err }

	for _, file := range rg.ancillaryFiles {
		outFile   := filepath.Clean(filepath.Join(rg.outRoot, filepath.Base(file)))
		f, err    := rg.fsys.Create(ctx, outFile)
		if err != nil                                        { return fmt.Errorf("cannot create %q: %w",     outFile, err) }
		data, err := fs.ReadFile(rg.embeddedFiles, file)
		if err != nil                                        { return fmt.Errorf("cannot read %q: %w",          file, err) }
		if _, err := fmt.Fprint(f, string(data)); err != nil { return fmt.Errorf("cannot write file %q: %w", outFile, err) }
		if err := f.Close(); err != nil { return fmt.Errorf("cannot close file %q: %w", outFile, err) }
	}

	return nil
}

// filterArgs discards any arguments up to and including "--" (potentially useful for testing)
func filterArgs(args []string) []string {
	for i, arg := range args {
		if arg == "--" {
			args = args[i+1:]
			break
		}
	}
	return args
}