//go:build windows

package gcs

import (
	"context"
	"encoding/binary"
	"encoding/json"
	"errors"
	"io"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/Microsoft/hcsshim/internal/gcs/prot"
	"github.com/Microsoft/hcsshim/internal/hcs"
	"github.com/sirupsen/logrus"
)

type stitched struct {
	io.ReadCloser
	io.WriteCloser
}

func (s *stitched) Close() error {
	s.ReadCloser.Close()
	s.WriteCloser.Close()
	return nil
}

func pipeConn() (*stitched, *stitched) {
	r1, w1 := io.Pipe()
	r2, w2 := io.Pipe()
	return &stitched{r1, w2}, &stitched{r2, w1}
}

func sendMessage(t *testing.T, w io.Writer, typ prot.MsgType, id int64, msg []byte) {
	t.Helper()
	var h [16]byte
	binary.LittleEndian.PutUint32(h[:], uint32(typ))
	binary.LittleEndian.PutUint32(h[4:], uint32(len(msg)+16))
	binary.LittleEndian.PutUint64(h[8:], uint64(id))
	_, err := w.Write(h[:])
	if err != nil {
		t.Error(err)
		return
	}
	_, err = w.Write(msg)
	if err != nil {
		t.Error(err)
		return
	}
}

func reflector(t *testing.T, rw io.ReadWriteCloser, delay time.Duration) {
	t.Helper()
	defer rw.Close()
	for {
		id, typ, msg, err := readMessage(rw)
		if err != nil {
			if !errors.Is(err, io.EOF) {
				t.Error(err)
			}
			return
		}
		time.Sleep(delay) // delay is used to test timeouts (when non-zero)
		typ ^= prot.MsgTypeResponse ^ prot.MsgTypeRequest
		sendMessage(t, rw, typ, id, msg)
	}
}

type testReq struct {
	prot.RequestBase
	X, Y int
}

type testResp struct {
	prot.ResponseBase
	X, Y int
}

func startReflectedBridge(t *testing.T, delay time.Duration) *bridge {
	t.Helper()
	s, c := pipeConn()
	b := newBridge(s, nil, logrus.NewEntry(logrus.StandardLogger()))
	b.Start()
	go reflector(t, c, delay)
	return b
}

func TestBridgeRPC(t *testing.T) {
	b := startReflectedBridge(t, 0)
	defer b.Close()
	req := testReq{X: 5}
	var resp testResp
	err := b.RPC(context.Background(), prot.RPCCreate, &req, &resp, false)
	if err != nil {
		t.Fatal(err)
	}
	if req.X != resp.X || req.Y != resp.Y {
		t.Fatalf("expected equal: %+v %+v", req, resp)
	}
}

func TestBridgeRPCResponseTimeout(t *testing.T) {
	b := startReflectedBridge(t, time.Minute)
	defer b.Close()
	b.Timeout = time.Millisecond * 100
	req := testReq{X: 5}
	var resp testResp
	err := b.RPC(context.Background(), prot.RPCCreate, &req, &resp, false)
	if err == nil || !strings.Contains(err.Error(), "bridge closed") {
		t.Fatalf("expected bridge disconnection, got %s", err)
	}
}

func TestBridgeRPCContextDone(t *testing.T) {
	b := startReflectedBridge(t, time.Minute)
	defer b.Close()
	b.Timeout = time.Millisecond * 250
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
	defer cancel()
	req := testReq{X: 5}
	var resp testResp
	err := b.RPC(ctx, prot.RPCCreate, &req, &resp, true)
	if err != context.DeadlineExceeded { //nolint:errorlint
		t.Fatalf("expected deadline exceeded, got %s", err)
	}
}

func TestBridgeRPCContextDoneNoCancel(t *testing.T) {
	b := startReflectedBridge(t, time.Minute)
	defer b.Close()
	b.Timeout = time.Millisecond * 250
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
	defer cancel()
	req := testReq{X: 5}
	var resp testResp
	err := b.RPC(ctx, prot.RPCCreate, &req, &resp, false)
	if err == nil || !strings.Contains(err.Error(), "bridge closed") {
		t.Fatalf("expected bridge disconnection, got %s", err)
	}
}

func TestBridgeRPCBridgeClosed(t *testing.T) {
	b := startReflectedBridge(t, 0)
	eerr := errors.New("forcibly terminated")
	b.kill(eerr)
	err := b.RPC(context.Background(), prot.RPCCreate, nil, nil, false)
	// Must wrap both the transport error and ErrBridgeClosed.
	if !errors.Is(err, eerr) {
		t.Fatalf("expected err to wrap eerr; got: %v", err)
	}
	if !errors.Is(err, ErrBridgeClosed) {
		t.Fatalf("expected err to wrap ErrBridgeClosed; got: %v", err)
	}
}

// TestBridgeKillWrapsSentinel verifies that once the bridge has been killed,
// any subsequent or in-flight operation surfaces an error recognizable as a
// closed bridge, so cleanup paths can safely give up after the guest dies
// instead of retrying forever.
func TestBridgeKillWrapsSentinel(t *testing.T) {
	b := startReflectedBridge(t, 0)
	transportErr := errors.New("bridge read failed: header read: EOF")
	b.kill(transportErr)

	if err := b.Wait(); !errors.Is(err, ErrBridgeClosed) || !errors.Is(err, transportErr) {
		t.Fatalf("Wait(): expected err to wrap both ErrBridgeClosed and transportErr; got: %v", err)
	}

	err := b.RPC(context.Background(), prot.RPCCreate, &testReq{}, &testResp{}, false)
	if !errors.Is(err, ErrBridgeClosed) {
		t.Fatalf("post-kill RPC: expected errors.Is(err, ErrBridgeClosed); got: %v", err)
	}
	if !errors.Is(err, transportErr) {
		t.Fatalf("post-kill RPC: expected errors.Is(err, transportErr); got: %v", err)
	}
}

func sendJSON(t *testing.T, w io.Writer, typ prot.MsgType, id int64, msg interface{}) error {
	t.Helper()
	msgb, err := json.Marshal(msg)
	if err != nil {
		return err
	}
	sendMessage(t, w, typ, id, msgb)
	return nil
}

func notifyThroughBridge(t *testing.T, typ prot.MsgType, msg interface{}, fn notifyFunc) error {
	t.Helper()
	s, c := pipeConn()
	b := newBridge(s, fn, logrus.NewEntry(logrus.StandardLogger()))
	b.Start()
	err := sendJSON(t, c, typ, 0, msg)
	if err != nil {
		b.Close()
		return err
	}
	time.Sleep(100 * time.Millisecond)
	return b.Close()
}

func TestBridgeNotify(t *testing.T) {
	ntf := &prot.ContainerNotification{Operation: "testing"}
	recvd := false
	err := notifyThroughBridge(t, prot.MsgTypeNotify|prot.ComputeSystem|prot.NotifyContainer, ntf, func(nntf *prot.ContainerNotification) error {
		if !reflect.DeepEqual(ntf, nntf) {
			t.Errorf("%+v != %+v", ntf, nntf)
		}
		recvd = true
		return nil
	})
	if err != nil {
		t.Error("notify failed: ", err)
	}
	if !recvd {
		t.Error("did not receive notification")
	}
}

func TestBridgeNotifyFailure(t *testing.T) {
	ntf := &prot.ContainerNotification{Operation: "testing"}
	errMsg := "notify should have failed"
	err := notifyThroughBridge(t, prot.MsgTypeNotify|prot.ComputeSystem|prot.NotifyContainer, ntf, func(nntf *prot.ContainerNotification) error {
		return errors.New(errMsg)
	})
	if err == nil || !strings.Contains(err.Error(), errMsg) {
		t.Error("unexpected result: ", err)
	}
}

// TestRPCErrorUnwrapHCSCode verifies that an rpcError carrying an HCS HRESULT
// with the high bit set (a negative int32, e.g. 0xc037010e) unwraps to an errno
// that errors.Is matches against the canonical syscall.Errno-based HCS
// constants. Regression test: a naive windows.Errno(int32) conversion
// sign-extends the value to 0xFFFFFFFF_C037010E, which fails to match
// hcs.ErrComputeSystemDoesNotExist.
func TestRPCErrorUnwrapHCSCode(t *testing.T) {
	// 0xc037010e == hcs.ErrComputeSystemDoesNotExist, stored as an int32 in the
	// bridge response Result field (decimal -1070137074).
	err := error(&rpcError{result: -1070137074}) // int32 form of 0xc037010e

	if !errors.Is(err, hcs.ErrComputeSystemDoesNotExist) {
		t.Fatalf("errors.Is(err, ErrComputeSystemDoesNotExist) = false; want true (err=%v)", err)
	}
	if !hcs.IsNotExist(err) {
		t.Fatalf("hcs.IsNotExist(err) = false; want true (err=%v)", err)
	}

	// A wrapped rpcError (as produced along real call chains) must still match.
	wrapped := errors.Join(errors.New("delete container state"), err)
	if !hcs.IsNotExist(wrapped) {
		t.Fatalf("hcs.IsNotExist(wrapped) = false; want true (err=%v)", wrapped)
	}
}

// TestPreregisterRPCReusesOutstanding verifies that pre-registering an id that
// is already outstanding hands back the existing call (a source rollback
// re-opens a process whose wait is still pending) rather than failing.
func TestPreregisterRPCReusesOutstanding(t *testing.T) {
	s, _ := pipeConn()
	b := newBridge(s, nil, logrus.NewEntry(logrus.StandardLogger()))

	first, err := b.PreregisterRPC(7, prot.RPCWaitForProcess, &testResp{})
	if err != nil {
		t.Fatalf("first PreregisterRPC = %v; want nil", err)
	}

	again, err := b.PreregisterRPC(7, prot.RPCWaitForProcess, &testResp{})
	if err != nil {
		t.Fatalf("duplicate PreregisterRPC = %v; want nil", err)
	}
	if again != first {
		t.Errorf("duplicate PreregisterRPC returned a new call; want the outstanding one")
	}
}
