// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package localexec

import (
	"fmt"
	"os"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/mitchellh/cli"
	"github.com/opentofu/opentofu/internal/provisioners"
	"github.com/zclconf/go-cty/cty"
)

func TestResourceProvider_Apply(t *testing.T) {
	defer os.Remove("test_out")
	output := cli.NewMockUi()
	p := New()
	schema := p.GetSchema().Provisioner
	c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
		"command": cty.StringVal("echo foo > test_out"),
	}))
	if err != nil {
		t.Fatal(err)
	}

	resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
		Config:   c,
		UIOutput: output,
	})

	if resp.Diagnostics.HasErrors() {
		t.Fatalf("err: %v", resp.Diagnostics.Err())
	}

	// Check the file
	raw, err := os.ReadFile("test_out")
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	actual := strings.TrimSpace(string(raw))
	expected := "foo"
	if actual != expected {
		t.Fatalf("bad: %#v", actual)
	}
}

func TestResourceProvider_stop(t *testing.T) {
	output := cli.NewMockUi()
	p := New()
	schema := p.GetSchema().Provisioner

	c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
		// bash/zsh/ksh will exec a single command in the same process. This
		// makes certain there's a subprocess in the shell.
		"command": cty.StringVal("sleep 30; sleep 30"),
	}))
	if err != nil {
		t.Fatal(err)
	}

	doneCh := make(chan struct{})
	startTime := time.Now()
	go func() {
		defer close(doneCh)
		// The functionality of p.Apply is tested in TestResourceProvider_Apply.
		// Because p.Apply is called in a goroutine, trying to t.Fatal() on its
		// result would be ignored or would cause a panic if the parent goroutine
		// has already completed.
		_ = p.ProvisionResource(provisioners.ProvisionResourceRequest{
			Config:   c,
			UIOutput: output,
		})
	}()

	mustExceed := (50 * time.Millisecond)
	select {
	case <-doneCh:
		t.Fatalf("expected to finish sometime after %s finished in %s", mustExceed, time.Since(startTime))
	case <-time.After(mustExceed):
		t.Logf("correctly took longer than %s", mustExceed)
	}

	// Stop it
	stopTime := time.Now()
	p.Stop()

	maxTempl := "expected to finish under %s, finished in %s"
	finishWithin := (2 * time.Second)
	select {
	case <-doneCh:
		t.Logf(maxTempl, finishWithin, time.Since(stopTime))
	case <-time.After(finishWithin):
		t.Fatalf(maxTempl, finishWithin, time.Since(stopTime))
	}
}

func TestResourceProvider_ApplyCustomInterpreter(t *testing.T) {
	output := cli.NewMockUi()
	p := New()

	schema := p.GetSchema().Provisioner

	c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
		"interpreter": cty.ListVal([]cty.Value{cty.StringVal("echo"), cty.StringVal("is")}),
		"command":     cty.StringVal("not really an interpreter"),
	}))
	if err != nil {
		t.Fatal(err)
	}

	resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
		Config:   c,
		UIOutput: output,
	})

	if resp.Diagnostics.HasErrors() {
		t.Fatal(resp.Diagnostics.Err())
	}

	got := strings.TrimSpace(output.OutputWriter.String())
	want := `Executing: ["echo" "is" "not really an interpreter"]
is not really an interpreter`
	if got != want {
		t.Errorf("wrong output\ngot:  %s\nwant: %s", got, want)
	}
}

func TestResourceProvider_ApplyCustomWorkingDirectory(t *testing.T) {
	testdir := "working_dir_test"
	os.Mkdir(testdir, 0755)
	defer os.Remove(testdir)

	output := cli.NewMockUi()
	p := New()
	schema := p.GetSchema().Provisioner

	command := "echo `pwd`"
	if runtime.GOOS == "windows" {
		command = "echo %cd%"
	}
	c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
		"working_dir": cty.StringVal(testdir),
		"command":     cty.StringVal(command),
	}))
	if err != nil {
		t.Fatal(err)
	}

	resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
		Config:   c,
		UIOutput: output,
	})

	if resp.Diagnostics.HasErrors() {
		t.Fatal(resp.Diagnostics.Err())
	}

	dir, err := os.Getwd()
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	got := strings.TrimSpace(output.OutputWriter.String())
	want := "Executing: [\"/bin/sh\" \"-c\" \"echo `pwd`\"]\n" + dir + "/" + testdir
	if runtime.GOOS == "windows" {
		want = "Executing: [\"cmd\" \"/C\" \"echo %cd%\"]\n" + dir + "\\" + testdir
	}
	if got != want {
		t.Errorf("wrong output\ngot:  %s\nwant: %s", got, want)
	}
}

func TestResourceProvider_ApplyCustomEnv(t *testing.T) {
	output := cli.NewMockUi()
	p := New()
	schema := p.GetSchema().Provisioner
	command := "echo $FOO $BAR $BAZ"
	if runtime.GOOS == "windows" {
		command = "echo %FOO% %BAR% %BAZ%"
	}
	c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
		"command": cty.StringVal(command),
		"environment": cty.MapVal(map[string]cty.Value{
			"FOO": cty.StringVal("BAR"),
			"BAR": cty.StringVal("1"),
			"BAZ": cty.StringVal("true"),
		}),
	}))
	if err != nil {
		t.Fatal(err)
	}

	resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
		Config:   c,
		UIOutput: output,
	})
	if resp.Diagnostics.HasErrors() {
		t.Fatal(resp.Diagnostics.Err())
	}

	got := strings.TrimSpace(output.OutputWriter.String())

	want := "Executing: [\"/bin/sh\" \"-c\" \"echo $FOO $BAR $BAZ\"]\nBAR 1 true"
	if runtime.GOOS == "windows" {
		want = "Executing: [\"cmd\" \"/C\" \"echo %FOO% %BAR% %BAZ%\"]\nBAR 1 true"
	}

	if got != want {
		t.Errorf("wrong output\ngot:  %s\nwant: %s", got, want)
	}
}

// Validate that Stop can Close can be called even when not provisioning.
func TestResourceProvisioner_StopClose(t *testing.T) {
	p := New()
	p.Stop()
	p.Close()
}

func TestResourceProvisioner_nullsInOptionals(t *testing.T) {
	output := cli.NewMockUi()
	p := New()
	schema := p.GetSchema().Provisioner

	for i, cfg := range []cty.Value{
		cty.ObjectVal(map[string]cty.Value{
			"command": cty.StringVal("echo OK"),
			"environment": cty.MapVal(map[string]cty.Value{
				"FOO": cty.NullVal(cty.String),
			}),
		}),
		cty.ObjectVal(map[string]cty.Value{
			"command":     cty.StringVal("echo OK"),
			"environment": cty.NullVal(cty.Map(cty.String)),
		}),
		cty.ObjectVal(map[string]cty.Value{
			"command":     cty.StringVal("echo OK"),
			"interpreter": cty.ListVal([]cty.Value{cty.NullVal(cty.String)}),
		}),
		cty.ObjectVal(map[string]cty.Value{
			"command":     cty.StringVal("echo OK"),
			"interpreter": cty.NullVal(cty.List(cty.String)),
		}),
		cty.ObjectVal(map[string]cty.Value{
			"command":     cty.StringVal("echo OK"),
			"working_dir": cty.NullVal(cty.String),
		}),
	} {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {

			cfg, err := schema.CoerceValue(cfg)
			if err != nil {
				t.Fatal(err)
			}

			// verifying there are no panics
			p.ProvisionResource(provisioners.ProvisionResourceRequest{
				Config:   cfg,
				UIOutput: output,
			})
		})
	}
}
