//go:build windows

package main

import (
	"context"
	"fmt"
	"math/rand"
	"os"
	"path/filepath"
	"strconv"
	"testing"
	"time"

	"github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options"
	"github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats"
	"github.com/Microsoft/hcsshim/internal/hcsoci"
	"github.com/Microsoft/hcsshim/pkg/ctrdtaskapi"
	"github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/runtime/v2/task"
	"github.com/containerd/typeurl"
	"github.com/opencontainers/runtime-spec/specs-go"
)

func setupTaskServiceWithFakes(t *testing.T) (*service, *testShimTask, *testShimExec) {
	t.Helper()
	tid := strconv.Itoa(rand.Int())

	s, err := NewService(WithTID(tid), WithIsSandbox(false))
	if err != nil {
		t.Fatalf("could not create service: %v", err)
	}

	// clean up the service
	t.Cleanup(func() {
		if _, err := s.shutdownInternal(context.Background(), &task.ShutdownRequest{
			ID:  s.tid,
			Now: true,
		}); err != nil {
			t.Fatalf("could not shutdown service: %v", err)
		}
	})

	task := &testShimTask{
		id:    tid,
		exec:  newTestShimExec(tid, tid, 10),
		execs: make(map[string]*testShimExec),
	}
	secondExecID := strconv.Itoa(rand.Int())
	secondExec := newTestShimExec(tid, secondExecID, 101)
	task.execs[secondExecID] = secondExec
	s.taskOrPod.Store(task)
	return s, task, secondExec
}

func Test_TaskShim_getTask_NotCreated_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: false,
	}

	st, err := s.getTask(t.Name())

	verifyExpectedError(t, st, err, errdefs.ErrNotFound)
}

func Test_TaskShim_getTask_Created_DifferentID_Error(t *testing.T) {
	s, _, _ := setupTaskServiceWithFakes(t)

	st, err := s.getTask("thisidwontmatch")

	verifyExpectedError(t, st, err, errdefs.ErrNotFound)
}

func Test_TaskShim_getTask_Created_CorrectID_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	st, err := s.getTask(t1.ID())
	if err != nil {
		t.Fatalf("should have not failed with error, got: %v", err)
	}
	if st != t1 {
		t.Fatal("should have returned a valid task")
	}
}

func Test_TaskShim_stateInternal_NoTask_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: false,
	}

	resp, err := s.stateInternal(context.TODO(), &task.StateRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_stateInternal_InitTaskID_DifferentExecID_Error(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.stateInternal(context.TODO(), &task.StateRequest{
		ID:     t1.ID(),
		ExecID: "thisshouldnotmatch",
	})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_stateInternal_InitTaskID_InitExecID_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.stateInternal(context.TODO(), &task.StateRequest{
		ID:     t1.ID(),
		ExecID: "",
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned StateResponse")
	}
	if resp.ID != t1.ID() {
		t.Fatalf("StateResponse.ID expected '%s' got '%s'", t1.ID(), resp.ID)
	}
	if resp.ExecID != t1.ID() {
		t.Fatalf("StateResponse.ExecID expected '%s' got '%s'", t1.ID(), resp.ExecID)
	}
	if resp.Pid != uint32(t1.exec.pid) {
		t.Fatalf("should have returned init pid, got: %v", resp.Pid)
	}
}

func Test_TaskShim_stateInternal_InitTaskID_2ndExecID_Success(t *testing.T) {
	s, t1, e2 := setupTaskServiceWithFakes(t)

	resp, err := s.stateInternal(context.TODO(), &task.StateRequest{
		ID:     t1.ID(),
		ExecID: e2.ID(),
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned StateResponse")
	}
	if resp.ID != t1.ID() {
		t.Fatalf("StateResponse.ID expected '%s' got '%s'", t1.ID(), resp.ID)
	}
	if resp.ExecID != e2.ID() {
		t.Fatalf("StateResponse.ExecID expected '%s' got '%s'", e2.ID(), resp.ExecID)
	}
	if resp.Pid != uint32(t1.execs[e2.ID()].pid) {
		t.Fatalf("should have returned 2nd exec pid, got: %v", resp.Pid)
	}
}

// TODO: Test_TaskShim_createInternal_*

func Test_TaskShim_startInternal_NoTask_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.startInternal(context.TODO(), &task.StartRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_startInternal_ValidTask_DifferentExecID_Error(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.startInternal(context.TODO(), &task.StartRequest{
		ID:     t1.ID(),
		ExecID: "thisshouldnotmatch",
	})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_startInternal_InitTaskID_InitExecID_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.startInternal(context.TODO(), &task.StartRequest{
		ID:     t1.ID(),
		ExecID: "",
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned StartResponse")
	}
	if resp.Pid != uint32(t1.exec.pid) {
		t.Fatal("should have returned init pid")
	}
}

func Test_TaskShim_startInternal_InitTaskID_2ndExecID_Success(t *testing.T) {
	s, t1, e2 := setupTaskServiceWithFakes(t)

	resp, err := s.startInternal(context.TODO(), &task.StartRequest{
		ID:     t1.ID(),
		ExecID: e2.ID(),
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned StartResponse")
	}
	if resp.Pid != uint32(t1.execs[e2.ID()].pid) {
		t.Fatal("should have returned 2nd pid")
	}
}

func Test_TaskShim_deleteInternal_NoTask_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.deleteInternal(context.TODO(), &task.DeleteRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_deleteInternal_ValidTask_DifferentExecID_Error(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.deleteInternal(context.TODO(), &task.DeleteRequest{
		ID:     t1.ID(),
		ExecID: "thisshouldnotmatch",
	})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_deleteInternal_InitTaskID_InitExecID_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.deleteInternal(context.TODO(), &task.DeleteRequest{
		ID:     t1.ID(),
		ExecID: "",
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned DeleteResponse")
	}
	if resp.Pid != uint32(t1.exec.pid) {
		t.Fatal("should have returned init pid")
	}
}

func Test_TaskShim_deleteInternal_InitTaskID_2ndExecID_Success(t *testing.T) {
	s, t1, e2 := setupTaskServiceWithFakes(t)

	// capture the t2 task as it will be deleted
	t2t := t1.execs[e2.ID()]

	resp, err := s.deleteInternal(context.TODO(), &task.DeleteRequest{
		ID:     t1.ID(),
		ExecID: e2.ID(),
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned DeleteResponse")
	}
	if resp.Pid != uint32(t2t.pid) {
		t.Fatal("should have returned 2nd pid")
	}
	if _, ok := t1.execs[e2.ID()]; ok {
		t.Fatal("should have deleted the 2nd exec")
	}
}

func Test_TaskShim_pidsInternal_NoTask_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.pidsInternal(context.TODO(), &task.PidsRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_pidsInternal_InitTaskID_Success(t *testing.T) {
	s, t1, e2 := setupTaskServiceWithFakes(t)

	resp, err := s.pidsInternal(context.TODO(), &task.PidsRequest{ID: t1.ID()})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned PidsResponse")
	}
	if len(resp.Processes) != 2 {
		t.Fatalf("should have returned len(processes) == 1, got: %v", len(resp.Processes))
	}
	if resp.Processes[0].Pid != uint32(t1.exec.pid) {
		t.Fatal("should have returned init pid")
	}
	if resp.Processes[0].Info == nil {
		t.Fatal("should have returned init pid info")
	}
	if resp.Processes[1].Pid != uint32(t1.execs[e2.ID()].pid) {
		t.Fatal("should have returned 2nd pid")
	}
	if resp.Processes[1].Info == nil {
		t.Fatal("should have returned 2nd pid info")
	}
	u, err := typeurl.UnmarshalAny(resp.Processes[1].Info)
	if err != nil {
		t.Fatalf("failed to unmarshal 2nd pid info, err: %v", err)
	}
	pi := u.(*options.ProcessDetails)
	if pi.ExecID != e2.ID() {
		t.Fatalf("should have returned 2nd pid ExecID, got: %v", pi.ExecID)
	}
}

func Test_TaskShim_pauseInternal_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.pauseInternal(context.TODO(), &task.PauseRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotImplemented)
}

func Test_TaskShim_resumeInternal_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.resumeInternal(context.TODO(), &task.ResumeRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotImplemented)
}

func Test_TaskShim_checkpointInternal_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.checkpointInternal(context.TODO(), &task.CheckpointTaskRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotImplemented)
}

func Test_TaskShim_killInternal_NoTask_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.killInternal(context.TODO(), &task.KillRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_killInternal_InitTaskID_DifferentExecID_Error(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.killInternal(context.TODO(), &task.KillRequest{
		ID:     t1.ID(),
		ExecID: "thisshouldnotmatch",
	})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_killInternal_InitTaskID_InitExecID_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.killInternal(context.TODO(), &task.KillRequest{
		ID:     t1.ID(),
		ExecID: "",
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned KillResponse")
	}
}

func Test_TaskShim_killInternal_InitTaskID_2ndExecID_Success(t *testing.T) {
	s, t1, e2 := setupTaskServiceWithFakes(t)

	resp, err := s.killInternal(context.TODO(), &task.KillRequest{
		ID:     t1.ID(),
		ExecID: e2.ID(),
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned KillResponse")
	}
}

// TODO: Test_TaskShim_execInternal_*

func Test_TaskShim_resizePtyInternal_NoTask_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.resizePtyInternal(context.TODO(), &task.ResizePtyRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_resizePtyInternal_InitTaskID_DifferentExecID_Error(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.resizePtyInternal(context.TODO(), &task.ResizePtyRequest{
		ID:     t1.ID(),
		ExecID: "thisshouldnotmatch",
	})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_resizePtyInternal_InitTaskID_InitExecID_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.resizePtyInternal(context.TODO(), &task.ResizePtyRequest{
		ID:     t1.ID(),
		ExecID: "",
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned ResizePtyResponse")
	}
}

func Test_TaskShim_resizePtyInternal_InitTaskID_2ndExecID_Success(t *testing.T) {
	s, t1, e2 := setupTaskServiceWithFakes(t)

	resp, err := s.resizePtyInternal(context.TODO(), &task.ResizePtyRequest{
		ID:     t1.ID(),
		ExecID: e2.ID(),
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned ResizePtyResponse")
	}
}

func Test_TaskShim_closeIOInternal_NoTask_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.closeIOInternal(context.TODO(), &task.CloseIORequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_closeIOInternal_InitTaskID_DifferentExecID_Error(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.closeIOInternal(context.TODO(), &task.CloseIORequest{
		ID:     t1.ID(),
		ExecID: "thisshouldnotmatch",
	})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_closeIOInternal_InitTaskID_InitExecID_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.closeIOInternal(context.TODO(), &task.CloseIORequest{
		ID:     t1.ID(),
		ExecID: "",
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned CloseIOResponse")
	}
}

func Test_TaskShim_closeIOInternal_InitTaskID_2ndExecID_Success(t *testing.T) {
	s, t1, e2 := setupTaskServiceWithFakes(t)

	resp, err := s.closeIOInternal(context.TODO(), &task.CloseIORequest{
		ID:     t1.ID(),
		ExecID: e2.ID(),
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned CloseIOResponse")
	}
}

func Test_TaskShim_updateInternal_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	var limit uint64 = 100
	resources := &specs.WindowsResources{
		Memory: &specs.WindowsMemoryResources{
			Limit: &limit,
		},
	}

	any, err := typeurl.MarshalAny(resources)
	if err != nil {
		t.Fatal(err)
	}

	resp, err := s.updateInternal(context.TODO(), &task.UpdateTaskRequest{ID: t1.ID(), Resources: any})
	if err != nil {
		t.Fatalf("should not have failed with error, got: %v", err)
	}
	if resp == nil {
		t.Fatalf("should have returned an empty resp")
	}
}

// Tests if a requested mount is valid for windows containers.
// Currently only host volumes/directories are supported to be mounted
// on a running windows container.
func Test_TaskShimWindowsMount_updateInternal_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)
	t1.isWCOW = true

	hostRWSharedDirectory := t.TempDir()
	fRW, _ := os.OpenFile(filepath.Join(hostRWSharedDirectory, "readwrite"), os.O_RDWR|os.O_CREATE, 0755)
	fRW.Close()

	resources := &ctrdtaskapi.ContainerMount{
		HostPath:      hostRWSharedDirectory,
		ContainerPath: hostRWSharedDirectory,
		ReadOnly:      true,
		Type:          "",
	}
	any, err := typeurl.MarshalAny(resources)
	if err != nil {
		t.Fatal(err)
	}

	resp, err := s.updateInternal(context.TODO(), &task.UpdateTaskRequest{ID: t1.ID(), Resources: any})
	if err != nil {
		t.Fatalf("should not have failed update mount with error, got: %v", err)
	}
	if resp == nil {
		t.Fatalf("should have returned an empty resp")
	}
}

func Test_TaskShimWindowsMount_updateInternal_Error(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)
	t1.isWCOW = true

	hostRWSharedDirectory := t.TempDir()
	tmpVhdPath := filepath.Join(hostRWSharedDirectory, "test-vhd.vhdx")

	fRW, _ := os.OpenFile(filepath.Join(tmpVhdPath, "readwrite"), os.O_RDWR|os.O_CREATE, 0755)
	fRW.Close()

	resources := &ctrdtaskapi.ContainerMount{
		HostPath:      tmpVhdPath,
		ContainerPath: tmpVhdPath,
		ReadOnly:      true,
		Type:          hcsoci.MountTypeVirtualDisk,
	}
	any, err := typeurl.MarshalAny(resources)
	if err != nil {
		t.Fatal(err)
	}

	resp, err := s.updateInternal(context.TODO(), &task.UpdateTaskRequest{ID: t1.ID(), Resources: any})
	if err == nil {
		t.Fatalf("should have failed update mount with error")
	}
	if resp != nil {
		t.Fatalf("should have returned a nil resp, got: %v", resp)
	}
}

func Test_TaskShim_updateInternal_Error(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	// resources must be of type *WindowsResources or *LinuxResources
	resources := &specs.Process{}
	any, err := typeurl.MarshalAny(resources)
	if err != nil {
		t.Fatal(err)
	}

	_, err = s.updateInternal(context.TODO(), &task.UpdateTaskRequest{ID: t1.ID(), Resources: any})
	if err == nil {
		t.Fatal("expected to get an error for incorrect resource's type")
	}
	if err != errNotSupportedResourcesRequest {
		t.Fatalf("expected to get errNotSupportedResourcesRequest, instead got %v", err)
	}
}

func Test_TaskShim_waitInternal_NoTask_Error(t *testing.T) {
	s := service{
		tid:       t.Name(),
		isSandbox: true,
	}

	resp, err := s.waitInternal(context.TODO(), &task.WaitRequest{ID: t.Name()})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_waitInternal_InitTaskID_DifferentExecID_Error(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.waitInternal(context.TODO(), &task.WaitRequest{
		ID:     t1.ID(),
		ExecID: "thisshouldnotmatch",
	})

	verifyExpectedError(t, resp, err, errdefs.ErrNotFound)
}

func Test_TaskShim_waitInternal_InitTaskID_InitExecID_Success(t *testing.T) {
	s, t1, _ := setupTaskServiceWithFakes(t)

	resp, err := s.waitInternal(context.TODO(), &task.WaitRequest{
		ID:     t1.ID(),
		ExecID: "",
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned WaitResponse")
	}
	if resp.ExitStatus != t1.exec.Status().ExitStatus {
		t.Fatal("should have returned exit status for init")
	}
}

func Test_TaskShim_waitInternal_InitTaskID_2ndExecID_Success(t *testing.T) {
	s, t1, e2 := setupTaskServiceWithFakes(t)

	resp, err := s.waitInternal(context.TODO(), &task.WaitRequest{
		ID:     t1.ID(),
		ExecID: e2.ID(),
	})
	if err != nil {
		t.Fatalf("should not have failed with error got: %v", err)
	}
	if resp == nil {
		t.Fatal("should have returned WaitResponse")
	}
	if resp.ExitStatus != t1.execs[e2.ID()].Status().ExitStatus {
		t.Fatal("should have returned exit status for init")
	}
}

func Test_TaskShim_statsInternal_InitTaskID_Success(t *testing.T) {
	testNames := []string{"WCOW", "LCOW"}
	for i, isWCOW := range []bool{true, false} {
		t.Run(testNames[i], func(t *testing.T) {
			s, t1, _ := setupTaskServiceWithFakes(t)
			t1.isWCOW = isWCOW

			resp, err := s.statsInternal(context.TODO(), &task.StatsRequest{ID: t1.ID()})

			if err != nil {
				t.Fatalf("should not have failed with error got: %v", err)
			}
			if resp == nil || resp.Stats == nil {
				t.Fatal("should have returned valid stats response")
			}
			statsI, err := typeurl.UnmarshalAny(resp.Stats)
			if err != nil {
				t.Fatalf("should not have failed to unmarshal StatsResponse got: %v", err)
			}
			stats := statsI.(*stats.Statistics)
			verifyExpectedStats(t, t1.isWCOW, true, stats)
		})
	}
}

func Test_TaskShim_shutdownInternal(t *testing.T) {
	for _, now := range []bool{true, false} {
		t.Run(fmt.Sprintf("%s_Now_%t", t.Name(), now), func(t *testing.T) {
			s, _, _ := setupTaskServiceWithFakes(t)

			if s.IsShutdown() {
				t.Fatal("service prematurely shutdown")
			}

			_, err := s.shutdownInternal(context.Background(), &task.ShutdownRequest{
				ID:  s.tid,
				Now: now,
			})
			if err != nil {
				t.Fatalf("could not shut down service: %v", err)
			}

			tm := time.NewTimer(5 * time.Millisecond)
			select {
			case <-tm.C:
				t.Fatalf("shutdown channel did not close")
			case <-s.Done():
				tm.Stop()
			}

			if !s.IsShutdown() {
				t.Fatal("service did not shutdown")
			}
		})
	}
}
