// 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"
	"net/url"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync/atomic"
	"syscall"
	"testing"
	"text/template"

	"github.com/graniticio/inifile"
	"github.com/jbunds/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

// 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
	useColor bool
}

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

// write performs a sticky-error write that optionally wraps the provided string in ANSI color codes.
func (w *errorWriter) writeColor(s, color string) {
	if w.e != nil { return }
	if !w.useColor {
		w.write(s)
		return
	}
	w.write(color)
	w.write(s)
	w.write("\033[0m") // reset attributes, styles, and colors to defaults
}

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

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

func newErrorWriter(w io.Writer) *errorWriter {
	return &errorWriter{
		w:        w,
		useColor: isTerm(w),
	}
}

// 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.
type workUnit struct {
	profile *cover.Profile
	outPath string
}

// reportGenerator orchestrates the generation of the HTML report.
type reportGenerator struct {
	fsys            writeFS
	embeddedFiles   fs.FS
	modFile         string
	modName         string
	repoURL         string
	outRoot         string
	profilePath     string
	profiles        []*cover.Profile
	pkgDirCache     map[string]string // pkgDirCache maps canonical Go packages to their respective paths on disk
	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
	ReadDir        (context.Context, string)                      ([]fs.DirEntry,  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 context-aware, 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) OpenWithContext(ctx context.Context, name string) (fs.File, error) {
	if err := ctx.Err(); err != nil { return nil, err }
	return lfs.Open(name) // expects callers pass filepath.Clean(name)
}

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(path, perm) // expects callers pass filepath.Clean(path)
}

func (lfs *localFS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
	if err := ctx.Err(); err != nil { return nil, err }
	return os.ReadDir(name) // expects callers pass filepath.Clean(name)
}

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) // expects callers pass filepath.Clean(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(name, data, perm) // expects callers pass 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() {
	os.Exit(run())
}

func run() int {
	ctx, stop := signal.NotifyContext(context.Background(),
		os.Interrupt,    // ctrl+c
		syscall.SIGTERM, // standard kill signal
		syscall.SIGHUP,  // terminal closed, SSH disconnection, etc
	)
	defer stop()

	var (
		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",
		}
	)

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

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

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

	repGen := &reportGenerator{
		fsys:           &localFS{},
		embeddedFiles:  embeddedFiles,
		modFile:        filepath.Clean(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)
		return 3
	}

	var gitConfig *inifile.IniConfig

	gitRoot, err := getGitRoot()
	if err != nil {
		fmt.Fprintf(os.Stderr, "cannot find Git root: %v\n", err)
		return 4
	}
	gitConfigPath := filepath.Join(gitRoot, ".git", "config")
	if gitConfig, err = inifile.NewIniConfigFromPath(gitConfigPath); err != nil {
		fmt.Fprintf(os.Stderr, "cannot parse Git config file (%q): %v\n", gitConfigPath, err)
		return 5
	}

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

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

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

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

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

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

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

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

	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)
		return 13
	}

	return 0
}

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

// getGitRoot locates the parent directory of the .git subdirectory.
func getGitRoot() (string, error) {
	cmd := exec.Command("git", "rev-parse", "--show-toplevel")
	out := new(bytes.Buffer)
	cmd.Stdout = out
	err := cmd.Run()
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(out.String()), 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 }

	if idx := strings.Index(gitRemoteURL, "@"); idx != -1 {
		gitRemoteURL = gitRemoteURL[idx + 1:]
	}

	gitRemoteURL = strings.TrimPrefix(gitRemoteURL, "ssh://")
	gitRemoteURL = strings.TrimPrefix(gitRemoteURL, "git://")

	if !strings.HasPrefix(gitRemoteURL, "http://" ) &&
	   !strings.HasPrefix(gitRemoteURL, "https://") {
		gitRemoteURL = "https://" + strings.Replace(gitRemoteURL, ":", "/", 1)
	}

	u, err := url.Parse(gitRemoteURL)
	if err != nil { return err }

	httpURL := fmt.Sprintf("https://%s%s", u.Host, u.Path)
	httpURL  = strings.TrimSuffix(httpURL, ".git")
	httpURL  = strings.TrimSuffix(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) ([]string, error) {
	if err := ctx.Err(); err != nil { return nil, err }

	f, err := rg.fsys.OpenWithContext(ctx, filepath.Clean(rg.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) error {
	if err := ctx.Err(); err != nil { return err }

	allPkgPaths, err := rg.getAllPkgPaths(ctx)
	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 }

	tempCache := make(map[string]string)

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

	rg.pkgDirCache = tempCache

	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, styleCSS string) 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{}{}
	}

	progDirs := progress.New(ctx, uint64(len(dirsToCreate)), progressOutput)
	defer progDirs.Close()

	group, gCtx := errgroup.WithContext(ctx)
	group.SetLimit(runtime.NumCPU()) // saturate available CPU threads for maximum throughput while bounding concurrent syscalls

	for dir := range dirsToCreate {
		if err := gCtx.Err(); err != nil { break } // group.Wait() below captures the error
		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.New(ctx, 0, progressOutput)
	defer progFiles.Close()

	group, gCtx = errgroup.WithContext(ctx)
	group.SetLimit(runtime.NumCPU()) // saturate available CPU threads for maximum throughput while bounding memory used by concurrent HTML buffers

	for i, unit := range units {
		if err := gCtx.Err(); err != nil { break } // group.Wait() below captures the error
		group.Go(func() error {

			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 := newErrorWriter(&buf)
			if err := rg.buildCovHTML(gCtx, ew, unit.profile, unit.profile.FileName, styleCSS); err != nil {
				return fmt.Errorf("cannot build HTML for %q: %w", unit.profile.FileName, err)
			}

			if err := rg.fsys.WriteFile(gCtx, 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, styleCSS 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[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
	})

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

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

	ew := newErrorWriter(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 "--".
func filterArgs(args []string) []string {
	for i, arg := range args {
		if arg == "--" {
			args = args[i + 1:]
			break
		}
	}
	return args
}

// isTerm determines if the specified writer is connected to a terminal.
func isTerm(v any) bool {
	if testing.Testing() { return false }
	if os.Getenv("GITHUB_ACTIONS") == "true" || // https://docs.github.com/actions/reference/workflows-and-actions/variables
	   os.Getenv("CI"            ) == "true" { return false }
	fd := getFD(v)
	if fd < 0 { return false }
	return term.IsTerminal(fd)
}

// getFD returns the file descriptor of the provided argument.
func getFD(w any) int {
	if f, ok := w.(interface{ Fd() uintptr }); ok { return int(f.Fd()) }
	return -1
}