package azalea_test

import (
	"errors"
	"fmt"
	"reflect"
	"strings"
	"testing"
	"unique"

	. "hakurei.app/internal/rosa/azalea"
)

// makeStackCheck creates a stack with a single frame with a single function "f"
// which calls the check function internally.
func makeStackCheck(check func(args FArgs) (any, error)) []Frame {
	return []Frame{{Func: map[unique.Handle[Ident]]F{
		unique.Make(Ident("f")): {F: func(
			args FArgs,
		) (v any, set bool, err error) {
			set = true
			v, err = check(args)
			return
		}},
	}}}
}

// checkArgs is like makeStackCheck, but the resulting function asserts that its
// args match the expected value.
func checkArgs(want FArgs) []Frame {
	return makeStackCheck(func(args FArgs) (any, error) {
		if !reflect.DeepEqual(args, want) {
			return nil, fmt.Errorf("%#v, want %#v", args, want)
		}
		return "\xfd", nil
	})
}

func TestEvaluate(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name string
		data string
		s    []Frame
		want any
		err  error
	}{
		{"apply unset", `f { v = unset; }`, makeStackCheck(func(
			args FArgs,
		) (v any, err error) {
			v = "\xfd"
			err = args.Apply(map[unique.Handle[Ident]]any{
				unique.Make(Ident("v")): &v,
			})
			return
		}), "\xfd", nil},

		{"apply bad type", `f { v = 9; }`, makeStackCheck(func(
			args FArgs,
		) (v any, err error) {
			v = "\xfd"
			err = args.Apply(map[unique.Handle[Ident]]any{
				unique.Make(Ident("v")): &v,
			})
			return
		}), "", TypeError{
			Concrete: reflect.TypeFor[int64](),
			Asserted: reflect.TypeFor[string](),
		}},

		{"apply undefined", `f { v = 9; }`, makeStackCheck(func(
			args FArgs,
		) (v any, err error) {
			v = "\xfd"
			err = args.Apply(map[unique.Handle[Ident]]any{})
			return
		}), "", EvaluationError{
			Expr: Func{
				Ident: Ident("f"),
				Args: []Arg{
					{K: []Ident{"v"}, V: Val{Int(9)}},
				},
			},
			Err: UndefinedError("v"),
		}},

		{"apply bound undefined", `f { _v# = "\x00"; v = _v; }`, makeStackCheck(func(
			args FArgs,
		) (v any, err error) {
			v = "\xfd"
			err = args.Apply(map[unique.Handle[Ident]]any{
				unique.Make(Ident("v")): &v,
			})
			return
		}), "\x00", nil},

		{"undefined function", `f {}`, nil, "", EvaluationError{
			Expr: Func{Ident: "f"},
			Err:  UndefinedError("f"),
		}},

		{"error wrap deep", `f { v = nil; }`, makeStackCheck(func(
			FArgs,
		) (any, error) {
			panic("unreachable")
		}), "", EvaluationError{
			Expr: Ident("nil"),
			Err:  UndefinedError("nil"),
		}},

		{"common inputs", `package name { inputs, v = []; }`, nil, "", EvaluationError{
			Expr: Func{
				Ident:   Ident("name"),
				Package: true,

				Args: []Arg{
					{K: []Ident{
						"inputs",
						"v",
					}, V: Val{Array(nil)}},
				},
			},
			Err: ErrInvalidSpecial,
		}},

		{"bound inputs", `package name { inputs# = []; }`, nil, "", EvaluationError{
			Expr: Func{
				Ident:   Ident("name"),
				Package: true,

				Args: []Arg{
					{K: []Ident{"inputs"}, V: Val{Array(nil)}, R: true},
				},
			},
			Err: ErrInvalidSpecial,
		}},

		{"bound runtime", `package name { runtime# = []; }`, nil, "", EvaluationError{
			Expr: Func{
				Ident:   Ident("name"),
				Package: true,

				Args: []Arg{
					{K: []Ident{"runtime"}, V: Val{Array(nil)}, R: true},
				},
			},
			Err: ErrInvalidSpecial,
		}},

		{"concat inputs", `package name { inputs = ""+""; }`, nil, "", EvaluationError{
			Expr: Func{
				Ident:   Ident("name"),
				Package: true,

				Args: []Arg{
					{K: []Ident{"inputs"}, V: Val{String(""), String("")}},
				},
			},
			Err: ErrInvalidSpecial,
		}},

		{"concat source", `package name { source = ""+""; }`, nil, "", EvaluationError{
			Expr: Func{
				Ident:   Ident("name"),
				Package: true,

				Args: []Arg{
					{K: []Ident{"source"}, V: Val{String(""), String("")}},
				},
			},
			Err: ErrInvalidSpecial,
		}},

		{"source handle", `package name { source = name; }`, nil, FArgs{
			{K: unique.Make(Ident("source")), V: Ident("name")},
		}, nil},

		{"integer array", `f { v = [ 0 ]; _v = [ 0, 9 ]; }`, checkArgs(FArgs{
			{K: unique.Make(Ident("v")), V: []int64{0}},
			{K: unique.Make(Ident("_v")), V: []int64{0, 9}},
		}), "\xfd", nil},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			var expr Func
			if e, err := Parse(strings.NewReader(tc.data)); err != nil {
				t.Fatal(err)
			} else if len(e) != 1 {
				t.Fatalf("got expression %#v", e)
			} else {
				expr = e[0].(Func)
			}
			const rPackage = "\xff\xff\xff\xff"
			r, set, err := Evaluate[string](func(
				name Ident,
				args FArgs,
			) (v any, set bool, err error) {
				v = rPackage
				if !reflect.DeepEqual(args, tc.want) {
					err = fmt.Errorf("%#v, want %#v", args, tc.want)
				}
				set = true
				return
			}, tc.s, expr)
			if set != (err == nil) {
				t.Error("Evaluate: unexpected unset")
			}

			if r != rPackage && r != tc.want {
				t.Errorf("Evaluate: %q, want %q", r, tc.want)
			}

			var errEquals bool
			if _, ok := errors.AsType[TypeError](err); ok {
				errEquals = errors.Is(err, tc.err)
			} else {
				errEquals = reflect.DeepEqual(err, tc.err)
			}
			if !errEquals {
				t.Errorf("Evaluate: error = %v, want %v", err, tc.err)
			}
		})
	}
}

func TestEvaluateGCC(t *testing.T) {
	t.Parallel()

	var gcc Func
	if e, err := Parse(strings.NewReader(sample)); err != nil {
		t.Fatal(err)
	} else {
		gcc = e[0].(Func)
	}

	var got [3]FArgs
	if r, set, err := Evaluate[string](func(
		name Ident,
		args FArgs,
	) (v any, set bool, err error) {
		v = "\x00"
		set = true
		got[0] = args
		return
	}, []Frame{{
		Func: map[unique.Handle[Ident]]F{
			unique.Make(Ident("remoteTar")): {F: func(
				args FArgs,
			) (v any, set bool, err error) {
				var url, checksum string
				var compress int
				if err = args.Apply(map[unique.Handle[Ident]]any{
					unique.Make(Ident("url")):      &url,
					unique.Make(Ident("checksum")): &checksum,
					unique.Make(Ident("compress")): &compress,
				}); err != nil {
					return
				}

				if compress != 0xcafe {
					err = fmt.Errorf("unexpected compress %#v", compress)
				}
				set = true
				v = url + "?checksum=" + checksum
				return
			}, V: map[unique.Handle[Ident]]any{
				unique.Make(Ident("gzip")): 0xcafe,
			}},

			unique.Make(Ident("make")): {F: func(
				args FArgs,
			) (v any, set bool, err error) {
				v = args
				set = true
				return
			}},

			unique.Make(Ident("arch")): {F: func(
				args FArgs,
			) (v any, set bool, err error) {
				set = false
				got[1] = args
				return
			}},

			unique.Make(Ident("noop")): {F: func(
				args FArgs,
			) (v any, set bool, err error) {
				set = false
				set = true
				got[2] = args
				return
			}, V: map[unique.Handle[Ident]]any{
				unique.Make(Ident("value")): "\xfd",
			}},
		},
	}}, gcc); err != nil {
		t.Fatal(err)
	} else if r != "\x00" {
		t.Fatalf("package: %q", r)
	} else if !set {
		t.Fatal("package: unset")
	}

	want := [...]FArgs{
		{
			{K: unique.Make(Ident("description")), V: "The GNU Compiler Collection"},
			{K: unique.Make(Ident("website")), V: "https://www.gnu.org/software/gcc"},
			{K: unique.Make(Ident("anitya")), V: int64(6502)},

			{K: unique.Make(Ident("version")), V: "16.1.0", R: true},
			{K: unique.Make(Ident("source")), V: "https://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-16.1.0/gcc-16.1.0.tar.gz?checksum=4ASoWbxaA2FW7PAB0zzHDPC5XnNhyaAyjtDPpGzceSLeYnEIXsNYZR3PA_Zu5P0K"},
			{K: unique.Make(Ident("patches")), V: []string{"musl-off64_t-loff_t.patch", "musl-legacy-lfs.patch"}},

			{K: unique.Make(Ident("exclusive")), V: true},
			{K: unique.Make(Ident("exec")), V: FArgs{
				{K: unique.Make(Ident("configure")), V: [][2]string{
					{"disable-multilib", ""},
					{"enable-default-pie", ""},
					{"disable-nls", ""},
					{"with-gnu-as", ""},
					{"with-gnu-ld", ""},
					{"with-system-zlib", ""},
					{"enable-languages", "c,c++,go"},
					{"with-native-system-header-dir", "/system/include"},
				}},
				{K: unique.Make(Ident("make")), V: []string{
					"BOOT_CFLAGS='-O2 -g'",
					"\x00",
					"bootstrap",
				}},
				{K: unique.Make(Ident("skip-check")), V: true},
			}},

			{K: unique.Make(Ident("inputs")), V: Array{
				{Ident("binutils")},
				{Ident("mpc")},
				{Ident("zlib")},
				{Ident("libucontext")},
				{Ident("kernel-headers")},
			}},
		},
		{
			{K: unique.Make(Ident("amd64")), V: "''"},
			{K: unique.Make(Ident("arm64")), V: "''"},
			{K: unique.Make(Ident("default"))},
		},
		{{K: unique.Make(Ident("key")), V: "\xfd"}},
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("package: args = %#v, want %#v", got, want)
	}
}

func BenchmarkEvaluate(b *testing.B) {
	var gcc Func
	if e, err := Parse(strings.NewReader(sample)); err != nil {
		b.Fatal(err)
	} else {
		gcc = e[0].(Func)
	}

	s := []Frame{{
		Func: map[unique.Handle[Ident]]F{
			unique.Make(Ident("remoteTar")): {F: func(
				FArgs,
			) (v any, set bool, err error) {
				return
			}, V: map[unique.Handle[Ident]]any{
				unique.Make(Ident("gzip")): 0xcafe,
			}},

			unique.Make(Ident("make")): {F: func(
				FArgs,
			) (v any, set bool, err error) {
				return
			}},

			unique.Make(Ident("arch")): {F: func(
				FArgs,
			) (v any, set bool, err error) {
				return
			}},

			unique.Make(Ident("noop")): {F: func(
				FArgs,
			) (v any, set bool, err error) {
				return
			}, V: map[unique.Handle[Ident]]any{
				unique.Make(Ident("value")): "\xfd",
			}},
		},
	}}
	for b.Loop() {
		if _, _, err := Evaluate[string](func(
			Ident,
			FArgs,
		) (v any, set bool, err error) {
			return
		}, s, gcc); err != nil {
			b.Fatal(err)
		}
	}
}
