//go:build windows

package exec

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/Microsoft/hcsshim/internal/conpty"
	"github.com/Microsoft/hcsshim/internal/jobobject"
)

func TestExec(t *testing.T) {
	// Exec a simple process and wait for exit.
	e, err := New(
		`C:\Windows\System32\ping.exe`,
		"ping 127.0.0.1",
		WithEnv(os.Environ()),
	)
	if err != nil {
		t.Fatal(err)
	}

	err = e.Start()
	if err != nil {
		t.Fatalf("failed to start process: %v", err)
	}

	err = e.Wait()
	if err != nil {
		t.Fatalf("error waiting for process: %v", err)
	}
	t.Logf("exit code was: %d", e.ExitCode())
}

func TestExecWithDir(t *testing.T) {
	// Test that the working directory is successfully set to whatever was passed in.
	dir := t.TempDir()

	e, err := New(
		`C:\Windows\System32\cmd.exe`,
		"cmd /c echo 'test' > test.txt",
		WithDir(dir),
		WithEnv(os.Environ()),
	)
	if err != nil {
		t.Fatal(err)
	}

	err = e.Start()
	if err != nil {
		t.Fatalf("failed to start process: %v", err)
	}

	err = e.Wait()
	if err != nil {
		t.Fatalf("error waiting for process: %v", err)
	}

	if _, err := os.Stat(filepath.Join(dir, "test.txt")); err != nil {
		t.Fatal(err)
	}

	t.Logf("exit code was: %d", e.ExitCode())
}

func TestExecStdinPowershell(t *testing.T) {
	// Exec a powershell instance and test that we can write commands to stdin and receive the output from stdout.
	e, err := New(
		`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`,
		"powershell",
		WithStdio(true, false, true),
		WithEnv(os.Environ()),
	)
	if err != nil {
		t.Fatal(err)
	}

	err = e.Start()
	if err != nil {
		t.Fatalf("failed to start process: %v", err)
	}

	errChan := make(chan error)
	go func() {
		_, _ = io.Copy(os.Stdout, e.Stdout())
	}()

	go func() {
		cmd := "ping 127.0.0.1\r\n"
		if _, err := e.Stdin().Write([]byte(cmd)); err != nil {
			errChan <- err
		}

		exit := "exit\r\n"
		if _, err := e.Stdin().Write([]byte(exit)); err != nil {
			errChan <- err
		}
		close(errChan)
	}()

	err = <-errChan
	if err != nil {
		t.Fatal(err)
	}

	waitChan := make(chan error)
	go func() {
		waitChan <- e.Wait()
	}()

	select {
	case err := <-waitChan:
		if err != nil {
			t.Fatalf("error waiting for process: %v", err)
		}
	case <-time.After(time.Second * 10):
		_ = e.Kill()
		t.Fatal("timed out waiting for process to complete")
	}

	t.Logf("exit code was: %d", e.ExitCode())
}

func TestExecsWithJob(t *testing.T) {
	// Test that we can assign processes to a job object at creation time.
	job, err := jobobject.Create(context.Background(), nil)
	if err != nil {
		t.Fatal(err)
	}
	defer job.Close()

	e, err := New(
		`C:\Windows\System32\ping.exe`,
		"ping -t 127.0.0.1",
		WithJobObject(job),
		WithStdio(false, false, false),
		WithEnv(os.Environ()),
	)
	if err != nil {
		t.Fatal(err)
	}

	err = e.Start()
	if err != nil {
		t.Fatalf("failed to start process: %v", err)
	}

	// Launch a second process and check pids.
	e2, err := New(
		`C:\Windows\System32\ping.exe`,
		"ping -t 127.0.0.1",
		WithJobObject(job),
		WithStdio(false, false, false),
		WithEnv(os.Environ()),
	)
	if err != nil {
		t.Fatal(err)
	}

	err = e2.Start()
	if err != nil {
		t.Fatalf("failed to start process: %v", err)
	}

	pidMap := map[int]struct{}{
		e.Pid():  {},
		e2.Pid(): {},
	}

	pids, err := job.Pids()
	if err != nil {
		t.Fatal(err)
	}

	if len(pids) != 2 {
		t.Fatalf("should be two pids in job object, got: %d. Pids: %+v", len(pids), pids)
	}

	for _, pid := range pids {
		if _, ok := pidMap[int(pid)]; !ok {
			t.Fatalf("failed to find pid %d in job object", pid)
		}
	}

	err = e.Kill()
	if err != nil {
		t.Fatalf("error killing process: %v", err)
	}

	err = e2.Kill()
	if err != nil {
		t.Fatalf("error killing process: %v", err)
	}

	_ = e.Wait()
	_ = e2.Wait()

	if !e.Exited() {
		t.Fatalf("Process %d should have exited after kill", e.Pid())
	}
	if !e2.Exited() {
		t.Fatalf("Process %d should have exited after kill", e2.Pid())
	}
}

func TestPseudoConsolePowershell(t *testing.T) {
	// This test is fairly flaky on the Github CI but seems to run fine locally. Skip this for now to let contributions continue without a hitch
	// and until we can replace this with a better suited test shortly.
	//
	// TODO(dcantah): Fix/find a better test here
	t.Skip("Skipping flaky test")
	cpty, err := conpty.Create(80, 20, 0)
	if err != nil {
		t.Fatal(err)
	}
	defer cpty.Close()

	// Exec a powershell instance and test that we can write commands to the input side of the pty and receive data
	// from the output end.
	e, err := New(
		`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`,
		"powershell",
		WithEnv(os.Environ()),
		WithConPty(cpty),
	)
	if err != nil {
		t.Fatal(err)
	}

	err = e.Start()
	if err != nil {
		t.Fatalf("failed to start process: %v", err)
	}

	errChan := make(chan error)
	go func() {
		_, _ = io.Copy(os.Stdout, cpty.OutPipe())
	}()

	go func() {
		cmd := "ping 127.0.0.1\r\n"
		if _, err := cpty.Write([]byte(cmd)); err != nil {
			errChan <- err
		}

		exit := "exit\r\n"
		if _, err := cpty.Write([]byte(exit)); err != nil {
			errChan <- err
		}
		close(errChan)
	}()

	err = <-errChan
	if err != nil {
		t.Fatal(err)
	}

	waitChan := make(chan error)
	go func() {
		waitChan <- e.Wait()
	}()

	select {
	case err := <-waitChan:
		if err != nil {
			t.Fatalf("error waiting for process: %v", err)
		}
	case <-time.After(time.Second * 10):
		_ = e.Kill()
		t.Fatal("timed out waiting for process to complete")
	}

	t.Logf("exit code was: %d", e.ExitCode())
}
