package nix_test

import (
	"errors"
	"io"
	"os"
	"os/exec"
	"slices"
	"strings"
	"testing"

	"gensokyo.uk/nix"
)

func TestInstantiated(t *testing.T) {
	stubNixCommand(t)

	testCases := []struct {
		name    string
		want    []string
		wantErr error
	}{
		{"bad fields", nil, &nix.MalformedInstantiatedError{Type: nix.InstantiatedBadFields}},
		{"unexpected quotes left", nil, &nix.MalformedInstantiatedError{Type: nix.InstantiatedUnexpectedQuotes}},
		{"unexpected quotes right", nil, &nix.MalformedInstantiatedError{Type: nix.InstantiatedUnexpectedQuotes}},
		{"unexpected quotes short", nil, &nix.MalformedInstantiatedError{Type: nix.InstantiatedUnexpectedQuotes}},
		{"not absolute", nil, &nix.MalformedInstantiatedError{Type: nix.InstantiatedNotAbsolute}},

		{"good segment", segmentWant, nil},

		{"getchoo atlas", getchooAtlasInstantiated, nil},
		{"getchoo glados", getchooGladosInstantiated, nil},
		{"pluiedev pappardelle", pluiedevPappardelleInstantiated, nil},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var stderr io.Writer
			if tc.wantErr != nil {
				stderr = os.Stderr
			}
			sample := instSample[tc.name]

			t.Run("decoder", func(t *testing.T) {
				out := strings.NewReader(sample)
				decoder := nix.NewInstantiatedDecoder(out, stderr)
				got, err := decoder.Decode()
				if !errors.Is(err, tc.wantErr) {
					t.Fatalf("Decode: error = %v, want %v", err, tc.wantErr)
				}
				if tc.wantErr != nil {
					t.Logf("Decode: error = %v", err)

					t.Run("scan after error", func(t *testing.T) {
						if decoder.Scan() {
							t.Fatalf("Scan unexpectedly succeeded on faulted decoder")
						}
					})
					return
				}
				if !slices.Equal(got, tc.want) {
					t.Errorf("Decode: %#v, want %#v", got, tc.want)
				}
			})

			t.Run("evaluator", func(t *testing.T) {
				ctx := newStubContext(t.Context(), nil, os.Stdout, stderr)
				got, err := nix.EvalInstantiated(ctx, tc.name)
				if !errors.Is(err, tc.wantErr) {
					t.Fatalf("EvalInstantiated: error = %v, want %v", err, tc.wantErr)
				}
				if tc.wantErr != nil {
					t.Logf("EvalInstantiated: error = %v", err)
					return
				}
				if !slices.Equal(got, tc.want) {
					t.Errorf("EvalInstantiated: %#v, want %#v", got, tc.want)
				}
			})
		})
	}
}

func stubInstantiatedEvaluator(args []string) error {
	_, _ = os.Stderr.Write([]byte(instSample[args[0]]))
	return nil
}

func TestInstantiatedDecoderStopEarly(t *testing.T) {
	decoder := nix.NewInstantiatedDecoder(strings.NewReader(segmentPrefix+segmentBody+segmentSuffix), os.Stderr)
	counter := 3
	got := make([]string, 0, counter)
	for drv := range decoder.Instantiated() {
		got = append(got, drv)
		counter--
		if counter == 0 {
			break
		}
	}

	if !slices.Equal(got, segmentEarly) {
		t.Errorf("Instantiated: %#v, want %#v", got, segmentEarly)
	}
}

func TestInstantiatedEvaluatorBadCommand(t *testing.T) {
	wantErr := os.ErrNotExist
	breakNixCommand(t)

	if _, err := nix.EvalInstantiated(
		nix.New(t.Context(), nil, os.Stdout, os.Stderr),
		"",
	); !errors.Is(err, wantErr) {
		t.Errorf("EvalInstantiated: error = %v, want %v", err, wantErr)
	}
}

func TestInstantiatedEvaluatorAlreadySet(t *testing.T) {
	stubNixCommand(t)
	if _, err := nix.EvalInstantiated(
		newStubContextCommand(func(cmd *exec.Cmd) { cmd.Stderr = os.Stderr }, t.Context(), nil, os.Stdout, os.Stderr),
		"",
	); err == nil {
		t.Errorf("EvalInstantiated unexpectedly succeeded")
	}
}

func TestMalformedInstantiatedError(t *testing.T) {
	t.Run("bad type", func(t *testing.T) {
		badErr := errors.New("")
		if errors.Is(new(nix.MalformedInstantiatedError), badErr) {
			t.Error("unexpected MalformedInstantiatedError equivalence")
		}
	})

	t.Run("nil", func(t *testing.T) {
		if errors.Is(new(nix.MalformedInstantiatedError), (*nix.MalformedInstantiatedError)(nil)) {
			t.Error("unexpected MalformedInstantiatedError equivalence")
		}
	})

	t.Run("unreachable", func(t *testing.T) {
		defer func() {
			wantPanic := "unreachable"
			if r := recover(); r != wantPanic {
				t.Errorf("Error: panic = %q, want %q", r, wantPanic)
			}
		}()
		_ = (&nix.MalformedInstantiatedError{Type: -1}).Error()
	})
}

func benchmarkInstantiatedDecoder(b *testing.B, out string) {
	decoder := nix.NewInstantiatedDecoder(strings.NewReader(out), nil)
	for b.Loop() {
	retry:
		if !decoder.Scan() {
			b.StopTimer()
			if err := decoder.Err(); err != nil {
				b.Fatalf("Decode: error = %v", err)
			}
			decoder = nix.NewInstantiatedDecoder(strings.NewReader(out), nil)
			b.StartTimer()
			goto retry
		}
		b.StopTimer()
		if !strings.HasPrefix(decoder.Text(), "/nix/store") {
			b.Fatalf("Text: unexpected prefix: %s", decoder.Text())
		}
		b.StartTimer()
	}
}

func BenchmarkInstantiatedDecoder(b *testing.B) {
	benchmarkInstantiatedDecoder(b, pluiedevPappardelleOut)
}

func benchmarkInstantiated(b *testing.B, out string, want []string) {
	for b.Loop() {
		v, _ := nix.NewInstantiatedDecoder(strings.NewReader(out), nil).Decode()
		b.StopTimer()
		if !slices.Equal(v, want) {
			b.Fatalf("Decode: %#v, want %#v", v, want)
		}
		b.StartTimer()
	}
}

func BenchmarkInstantiated(b *testing.B) {
	/* 27750 raw, 10729 deduplicated */
	benchmarkInstantiated(b, getchooGladosOut, getchooGladosInstantiated)
}

func BenchmarkInstantiatedCopy(b *testing.B) {
	/* 40177 raw, 10685 deduplicated */
	benchmarkInstantiated(b, pluiedevPappardelleOut, pluiedevPappardelleInstantiated)
}
