package core

import (
	"strings"
	"testing"
	"time"
)

func TestExecute(t *testing.T) {
	assertLen := func(t *testing.T, items []Item, length int) {
		t.Helper()
		if len(items) != length {
			t.Errorf("Expected %d items, got %d", length, len(items))
		}
	}
	assertNil := func(t *testing.T, err error) {
		t.Helper()
		if err != nil {
			t.Fatal(err)
		}
	}
	assertNotNil := func(t *testing.T, err error) {
		t.Helper()
		if err == nil {
			t.Error("expected err")
		}
	}
	execute := func(argv []string) ([]Item, error) {
		item, _, _, err := Execute("_", argv, nil, nil, "", time.Minute)
		return item, err
	}

	t.Run("Noop", func(t *testing.T) {
		res, err := execute([]string{"true"})
		assertNil(t, err)
		assertLen(t, res, 0)
	})

	t.Run("ExitWithErrorCode", func(t *testing.T) {
		res, err := execute([]string{"false"})
		assertNotNil(t, err)
		assertLen(t, res, 0)
	})

	t.Run("ExitWithSpecificErrorCode", func(t *testing.T) {
		res, err := execute([]string{"sh", "-c", "exit 22"})
		assertNotNil(t, err)
		assertLen(t, res, 0)
	})

	t.Run("Timeout", func(t *testing.T) {
		res, _, _, err := Execute("_", []string{"sleep", "10"}, nil, nil, "", time.Millisecond)
		assertNotNil(t, err)
		assertLen(t, res, 0)
	})

	t.Run("ReturningItems", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `{id: "foo"}`})
		assertNil(t, err)
		assertLen(t, res, 1)
		if res[0].Id != "foo" {
			t.Fatal("jq -cn test failed")
		}
	})

	t.Run("ReadFromStdin", func(t *testing.T) {
		res, _, _, err := Execute("_", []string{"jq", "-cR", `{id: .}`}, nil, nil, "bar", time.Minute)
		assertNil(t, err)
		assertLen(t, res, 1)
		if res[0].Id != "bar" {
			t.Fatal("jq -cR test failed")
		}
	})

	t.Run("SetEnv", func(t *testing.T) {
		res, _, _, err := Execute("_", []string{"jq", "-cn", `{id: env.HELLO}`}, []string{"HELLO=baz"}, nil, "", time.Minute)
		assertNil(t, err)
		assertLen(t, res, 1)
		if res[0].Id != "baz" {
			t.Fatal("jq -cn env test failed")
		}
	})
	t.Run("StderrLogging", func(t *testing.T) {
		res, err := execute([]string{"sh", "-c", `echo 1>&2 Hello; jq -cn '{id: "box"}'; echo 1>&2 World`})
		assertNil(t, err)
		assertLen(t, res, 1)
		if res[0].Id != "box" {
			t.Fatal("stderr test failed")
		}
	})

	t.Run("UnknownFieldIgnored", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `{id: "test", unknownField: "what is this"}`})
		assertNil(t, err)
		assertLen(t, res, 1)
	})

	t.Run("IncorrectIdType", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `{id: ["list"]}`})
		assertNotNil(t, err)
		assertLen(t, res, 0)
	})

	t.Run("IncorrectTimeType", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `{id: "test", time: "0"}`})
		assertNotNil(t, err)
		assertLen(t, res, 0)
	})

	t.Run("NullId", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `{id: null}`})
		assertNotNil(t, err)
		assertLen(t, res, 0)
	})

	t.Run("DuplicateItemIds", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `["a", "a"] | .[] | {id: .}`})
		assertNotNil(t, err)
		assertLen(t, res, 0)
	})

	t.Run("ActionNullValueOk", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `{id: "test", action: {"hello": null}}`})
		assertNil(t, err)
		assertLen(t, res, 1)
		if res[0].Action["hello"] == nil {
			t.Fatal("missing hello action")
		}
		if res[0].Action["goodbye"] != nil {
			t.Fatal("nonexistent action should key to nil in Action")
		}
	})

	t.Run("ActionEmptyStringOk", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `{id: "test", action: {"hello": ""}}`})
		assertNil(t, err)
		assertLen(t, res, 1)
		if res[0].Action["hello"] == nil {
			t.Fatal("missing hello action")
		}
	})

	t.Run("ActionEmptyArrayOk", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `{id: "test", action: {"hello": []}}`})
		assertNil(t, err)
		assertLen(t, res, 1)
		if res[0].Action["hello"] == nil {
			t.Fatal("missing hello action")
		}
	})

	t.Run("ActionEmptyObjectOk", func(t *testing.T) {
		res, err := execute([]string{"jq", "-cn", `{id: "test", action: {"hello": {}}}`})
		assertNil(t, err)
		assertLen(t, res, 1)
		if res[0].Action["hello"] == nil {
			t.Fatal("missing hello action")
		}
	})

	t.Run("ReadState", func(t *testing.T) {
		argv := []string{"sh", "-c", `cat $STATE_PATH | jq -cR '{id: "greeting", title: .} | .title = "Hello " + .title'`}
		res, _, _, err := Execute("_", argv, nil, []byte("world"), "", time.Minute)
		assertNil(t, err)
		assertLen(t, res, 1)
		if res[0].Title != "Hello world" {
			t.Fatalf("expected 'Hello world' from read state, got '%s'", res[0].Title)
		}
	})

	t.Run("WriteState", func(t *testing.T) {
		argv := []string{"sh", "-c", `printf "Hello world" > $STATE_PATH; jq -cn '{id: "test"}'`}
		res, newState, _, err := Execute("_", argv, nil, nil, "", time.Minute)
		assertNil(t, err)
		assertLen(t, res, 1)
		if string(newState) != "Hello world" {
			t.Fatalf("expected 'Hello world' from write state, got %s", string(newState))
		}
	})

	t.Run("ErrorItem", func(t *testing.T) {
		argv := []string{"sh", "-c", `echo 1>&2 Hello; jq -cn '{id: "box"}'; echo 1>&2 World; printf '{"whoops": "my bad"}'`}
		_, _, errItem, err := Execute("test", argv, nil, nil, "", time.Minute)
		assertNotNil(t, err)
		if errItem.Id == "" {
			t.Error("missing erritem id")
		}
		if errItem.Source != "default" {
			t.Errorf("unexpected erritem source: expected default, got %s", errItem.Source)
		}
		if !strings.Contains(errItem.Body, "Hello") || !strings.Contains(errItem.Body, "World") {
			t.Error("missing stderr from erritem")
		}
		if !strings.Contains(errItem.Body, "whoops") {
			t.Error("missing stdout from erritem")
		}
	})
}