package runner

import (
	"bytes"
	"context"
	"fmt"
	"os/exec"
)

// Spec describes a single command to run.
type Spec struct {
	Name  string
	Args  []string
	Dir   string
	Env   []string
	Stdin []byte
}

// Runner runs external commands. Production code uses Real; tests use Fake.
type Runner interface {
	Run(ctx context.Context, s Spec) ([]byte, error)
}

// Real executes via os/exec and returns combined stdout+stderr.
type Real struct{}

func (Real) Run(ctx context.Context, s Spec) ([]byte, error) {
	cmd := exec.CommandContext(ctx, s.Name, s.Args...)
	if s.Dir != "" {
		cmd.Dir = s.Dir
	}
	if len(s.Env) > 0 {
		cmd.Env = s.Env
	}
	if len(s.Stdin) > 0 {
		cmd.Stdin = bytes.NewReader(s.Stdin)
	}
	out, err := cmd.CombinedOutput()
	if err != nil {
		return out, fmt.Errorf("%s %v: %w (output: %s)", s.Name, s.Args, err, string(out))
	}
	return out, nil
}

// Call is a recorded invocation made against a Fake.
type Call struct {
	Name string
	Args []string
	Dir  string
}

type fakeResult struct {
	out []byte
	err error
}

// Fake records calls and returns configured results.
type Fake struct {
	Calls   []Call
	results map[string]fakeResult
}

// On configures a result for the given name + args. nil args matches any.
func (f *Fake) On(name string, args []string) *FakeBuilder {
	return &FakeBuilder{f: f, name: name, args: args}
}

type FakeBuilder struct {
	f    *Fake
	name string
	args []string
}

func (b *FakeBuilder) Return(out []byte, err error) {
	if b.f.results == nil {
		b.f.results = map[string]fakeResult{}
	}
	b.f.results[fakeKey(b.name, b.args)] = fakeResult{out: out, err: err}
}

func fakeKey(name string, args []string) string {
	if args == nil {
		return name + "\x00*"
	}
	k := name
	for _, a := range args {
		k += "\x00" + a
	}
	return k
}

func (f *Fake) Run(ctx context.Context, s Spec) ([]byte, error) {
	f.Calls = append(f.Calls, Call{Name: s.Name, Args: append([]string(nil), s.Args...), Dir: s.Dir})
	if r, ok := f.results[fakeKey(s.Name, s.Args)]; ok {
		return r.out, r.err
	}
	if r, ok := f.results[fakeKey(s.Name, nil)]; ok {
		return r.out, r.err
	}
	return nil, fmt.Errorf("fake runner: no result configured for %s %v", s.Name, s.Args)
}
