/*
 * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package spawn

import (
	"bytes"
	"encoding/json"
	"log"
	"os"
	"os/exec"
	"reflect"
	"strings"
	"testing"

	"github.com/BBVA/kapow/internal/server/model"
)

type Output struct {
	Cmdline []string          `json:"cmdline"`
	Env     map[string]string `json:"env"`
}

func decodeJailLover(out []byte) (jldata Output) {
	err := json.Unmarshal(out, &jldata)
	if err != nil {
		log.Fatal("jaillover output is malformed: ", err)
	}
	return
}

func locateJailLover() string {
	out, err := exec.Command("which", "jaillover").Output()
	if err != nil {
		log.Fatal("jaillover not found in PATH", err)
	}
	return strings.TrimRight(string(out), "\n")
}

func TestSpawnRetursErrorWhenEntrypointIsBad(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: "/bin/this_executable_is_not_likely_to_exist",
		},
	}

	err := Spawn(h, nil, nil)

	if err == nil {
		t.Error("Bad executable not reported")
	}
}

func TestSpawnReturnsNilWhenEntrypointIsGood(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: locateJailLover(),
		},
	}

	err := Spawn(h, nil, nil)

	if err != nil {
		t.Error("Good executable reported")
	}
}

func TestSpawnWritesToStdout(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: locateJailLover(),
		},
	}
	out := &bytes.Buffer{}

	_ = Spawn(h, out, nil)

	jldata := decodeJailLover(out.Bytes())
	if jldata.Cmdline[0] != locateJailLover() {
		t.Error("Output does not match jaillover's")
	}
}

func TestSpawnWritesToStderr(t *testing.T) {
	expected := "jailover miserably failed\n"
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: locateJailLover() + " --miserably-fail",
		},
	}
	stderr := &bytes.Buffer{}

	_ = Spawn(h, nil, stderr)

	jldata := stderr.String()
	if jldata != expected {
		t.Errorf("Error does not match jaillover's. Expected: #%s#, got: #%s#", expected, jldata)
	}
}

func TestSpawnSetsKapowURLEnvVar(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: locateJailLover(),
		},
	}
	out := &bytes.Buffer{}

	os.Setenv("KAPOW_DATA_URL", "http://localhost:8082")

	_ = Spawn(h, out, nil)

	os.Unsetenv("KAPOW_DATA_URL")

	jldata := decodeJailLover(out.Bytes())
	if v, ok := jldata.Env["KAPOW_DATA_URL"]; !ok || v != "http://localhost:8082" {
		t.Error("KAPOW_DATA_URL is not set properly")
	}
}

func TestSpawnSetsKapowHandlerIDEnvVar(t *testing.T) {
	h := &model.Handler{
		ID: "HANDLER_ID_FOO",
		Route: model.Route{
			Entrypoint: locateJailLover(),
		},
	}
	out := &bytes.Buffer{}

	_ = Spawn(h, out, nil)

	jldata := decodeJailLover(out.Bytes())
	if v, ok := jldata.Env["KAPOW_HANDLER_ID"]; !ok || v != "HANDLER_ID_FOO" {
		t.Error("KAPOW_HANDLER_ID is not set properly")
	}
}

func TestSpawnRunsOKEntrypointsWithAParam(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: locateJailLover() + " -foo",
		},
	}
	out := &bytes.Buffer{}

	_ = Spawn(h, out, nil)

	jldata := decodeJailLover(out.Bytes())
	if !reflect.DeepEqual(jldata.Cmdline, []string{locateJailLover(), "-foo"}) {
		t.Error("Args not as expected")
	}
}

func TestSpawnRunsOKEntrypointWithArgWithSpace(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: locateJailLover() + ` "foo bar"`,
		},
	}
	out := &bytes.Buffer{}

	_ = Spawn(h, out, nil)

	jldata := decodeJailLover(out.Bytes())
	if !reflect.DeepEqual(jldata.Cmdline, []string{locateJailLover(), "foo bar"}) {
		t.Error("Args not parsed as expected")
	}
}

func TestSpawnErrorsWhenEntrypointIsInvalidShell(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: locateJailLover() + ` "`,
		},
	}
	out := &bytes.Buffer{}

	err := Spawn(h, out, nil)

	if err == nil {
		t.Error("Invalid args not reported")
	}
}

func TestSpawnRunsOKEntrypointWithMultipleArgs(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: locateJailLover() + " foo bar",
		},
	}
	out := &bytes.Buffer{}

	_ = Spawn(h, out, nil)

	jldata := decodeJailLover(out.Bytes())
	if !reflect.DeepEqual(jldata.Cmdline, []string{locateJailLover(), "foo", "bar"}) {
		t.Error("Args not parsed as expected")
	}
}

func TestSpawnRunsOKEntrypointAndCommand(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{
			Entrypoint: locateJailLover() + " foo bar",
			Command:    "baz qux",
		},
	}
	out := &bytes.Buffer{}

	_ = Spawn(h, out, nil)

	jldata := decodeJailLover(out.Bytes())
	if !reflect.DeepEqual(jldata.Cmdline, []string{locateJailLover(), "foo", "bar", "baz qux"}) {
		t.Error("Malformed cmdline")
	}
}

func TestSpawnReturnsErrorIfEntrypointNotSet(t *testing.T) {
	h := &model.Handler{
		Route: model.Route{},
	}

	err := Spawn(h, nil, nil)

	if err == nil {
		t.Error("Spawn() did not report entrypoint not set")
	}
}
