package nix_test

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"slices"
	"strings"
	"syscall"
	"testing"

	"gensokyo.uk/nix"
	"hakurei.app/command"
)

const (
	runAsNixStub = "TEST_RUN_AS_NIX_STUB"
)

var (
	stubExtraArgs = []string{
		"-test.run=TestNixStub",
		"--",
	}
)

// stubNixCommand causes all nix command invocations to invoke the stub for the current test.
func stubNixCommand(t *testing.T) {
	n := nix.Nix
	nix.Nix = os.Args[0]
	t.Cleanup(func() { nix.Nix = n })

	cur, ok := os.LookupEnv(runAsNixStub)
	if err := os.Setenv(runAsNixStub, "1"); err != nil {
		t.Fatalf("cannot setenv: %v", err)
	}
	t.Cleanup(func() {
		if !ok {
			if err := os.Unsetenv(runAsNixStub); err != nil {
				t.Fatalf("cannot unsetenv: %v", err)
			}
			return
		}
		if err := os.Setenv(runAsNixStub, cur); err != nil {
			t.Fatalf("cannot setenv: %v", err)
		}
	})
}

// newStubContext creates a context for use with the nix command stub.
func newStubContext(ctx context.Context, extraArgs []string, stdout, stderr io.Writer) nix.Context {
	return nix.New(ctx, append(stubExtraArgs, extraArgs...), stdout, stderr)
}

type stubContextCommand struct {
	f func(*exec.Cmd)

	nix.Context
}

func (s *stubContextCommand) Nix(ctx context.Context, arg ...string) *exec.Cmd {
	cmd := s.Context.Nix(ctx, arg...)
	if s.f != nil {
		s.f(cmd)
	}
	return cmd
}

// newStubContext creates a context for use with the nix command stub with a function injected into [nixbuild.Context.Nix].
func newStubContextCommand(f func(*exec.Cmd), ctx context.Context, extraArgs []string, stdout, stderr io.Writer) nix.Context {
	return &stubContextCommand{f, newStubContext(ctx, extraArgs, stdout, stderr)}
}

// breakNixCommand makes all nix invocations fail for the current test.
func breakNixCommand(t *testing.T) {
	n := nix.Nix
	nix.Nix = "/proc/nonexistent"
	t.Cleanup(func() { nix.Nix = n })
}

type stubCommandInitFunc func(c command.Command)

var stubCommandInit []stubCommandInitFunc

// this test mocks the nix command
func TestNixStub(t *testing.T) {
	if os.Getenv(runAsNixStub) != "1" {
		return
	}

	var (
		flagExtraExperimentalFeatures string
	)
	c := command.New(os.Stderr, t.Logf, "nix", func(args []string) error {
		if flagExtraExperimentalFeatures != nix.ExperimentalFeaturesFlakes {
			t.Fatalf("%s: %q, want %q",
				nix.ExtraExperimentalFeatures, flagExtraExperimentalFeatures, nix.ExperimentalFeaturesFlakes)
			return syscall.ENOTRECOVERABLE
		}
		return nil
	}).
		Flag(&flagExtraExperimentalFeatures, trimFlagName(nix.ExtraExperimentalFeatures), command.StringFlag(""),
			fmt.Sprintf("expects exactly %q", nix.ExperimentalFeaturesFlakes))

	c.Command("true", command.UsageInternal, func([]string) error { return nil })

	for _, f := range stubCommandInit {
		f(c)
	}

	c.MustParse(os.Args[len(stubExtraArgs)+1:], func(err error) {
		if err != nil {
			t.Fatal(err.Error())
		}
	})
}

// checkStdin checks whether entries read from r is equivalent to want.
func checkStdin(r io.Reader, want ...string) error {
	if got, err := nix.ReadStdin(r); err != nil {
		return err
	} else if !slices.Equal(got, want) {
		return errors.New(fmt.Sprintf("got build %#v, want %#v", got, want))
	}
	return nil
}

func trimFlagName(n string) string { return strings.TrimPrefix(n, "--") }

// errorWriter unconditionally returns a non-nil error
type errorWriter struct{}

func (errorWriter) Write([]byte) (int, error) { return 0, syscall.EIO }
