package client

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"testing"

	cerrdefs "github.com/containerd/errdefs"
	"github.com/moby/moby/api/types/container"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

func TestExecCreateError(t *testing.T) {
	client, err := New(
		WithMockClient(errorMock(http.StatusInternalServerError, "Server error")),
	)
	assert.NilError(t, err)

	_, err = client.ExecCreate(t.Context(), "container_id", ExecCreateOptions{})
	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))

	_, err = client.ExecCreate(t.Context(), "", ExecCreateOptions{})
	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
	assert.Check(t, is.ErrorContains(err, "value is empty"))

	_, err = client.ExecCreate(t.Context(), "    ", ExecCreateOptions{})
	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
	assert.Check(t, is.ErrorContains(err, "value is empty"))
}

// TestExecCreateConnectionError verifies that connection errors occurring
// during API-version negotiation are not shadowed by API-version errors.
//
// Regression test for https://github.com/docker/cli/issues/4890
func TestExecCreateConnectionError(t *testing.T) {
	client, err := New(WithHost("tcp://no-such-host.invalid"))
	assert.NilError(t, err)

	_, err = client.ExecCreate(t.Context(), "container_id", ExecCreateOptions{})
	assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
}

func TestExecCreate(t *testing.T) {
	const expectedURL = "/containers/container_id/exec"
	client, err := New(
		WithMockClient(func(req *http.Request) (*http.Response, error) {
			if err := assertRequest(req, http.MethodPost, expectedURL); err != nil {
				return nil, err
			}
			// FIXME validate the content is the given ExecConfig ?
			if err := req.ParseForm(); err != nil {
				return nil, err
			}
			execConfig := &container.ExecCreateRequest{}
			if err := json.NewDecoder(req.Body).Decode(execConfig); err != nil {
				return nil, err
			}
			if execConfig.User != "user" {
				return nil, fmt.Errorf("expected an execConfig with User == 'user', got %v", execConfig)
			}
			return mockJSONResponse(http.StatusOK, nil, container.ExecCreateResponse{
				ID: "exec_id",
			})(req)
		}),
	)
	assert.NilError(t, err)

	res, err := client.ExecCreate(t.Context(), "container_id", ExecCreateOptions{
		User: "user",
	})
	assert.NilError(t, err)
	assert.Check(t, is.Equal(res.ID, "exec_id"))
}

func TestExecStartError(t *testing.T) {
	client, err := New(
		WithMockClient(errorMock(http.StatusInternalServerError, "Server error")),
	)
	assert.NilError(t, err)

	_, err = client.ExecStart(t.Context(), "nothing", ExecStartOptions{})
	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}

func TestExecStart(t *testing.T) {
	const expectedURL = "/exec/exec_id/start"
	client, err := New(
		WithMockClient(func(req *http.Request) (*http.Response, error) {
			if err := assertRequest(req, http.MethodPost, expectedURL); err != nil {
				return nil, err
			}
			if err := req.ParseForm(); err != nil {
				return nil, err
			}
			request := &container.ExecStartRequest{}
			if err := json.NewDecoder(req.Body).Decode(request); err != nil {
				return nil, err
			}
			if request.Tty || !request.Detach {
				return nil, fmt.Errorf("expected ExecStartOptions{Detach:true,Tty:false}, got %v", request)
			}
			return mockResponse(http.StatusOK, nil, "")(req)
		}),
	)
	assert.NilError(t, err)

	_, err = client.ExecStart(t.Context(), "exec_id", ExecStartOptions{
		Detach: true,
		TTY:    false,
	})
	assert.NilError(t, err)
}

func TestExecStartConsoleSize(t *testing.T) {
	tests := []struct {
		doc     string
		options ExecStartOptions
		expErr  string
		expReq  container.ExecStartRequest
	}{
		{
			doc: "without TTY",
			options: ExecStartOptions{
				Detach:      true,
				TTY:         false,
				ConsoleSize: ConsoleSize{Height: 100, Width: 200},
			},
			expErr: "console size is only supported when TTY is enabled",
		},
		{
			doc: "with TTY",
			options: ExecStartOptions{
				Detach:      true,
				TTY:         true,
				ConsoleSize: ConsoleSize{Height: 100, Width: 200},
			},
			expReq: container.ExecStartRequest{
				Detach:      true,
				Tty:         true,
				ConsoleSize: &[2]uint{100, 200},
			},
		},
	}
	for _, tc := range tests {
		t.Run(tc.doc, func(t *testing.T) {
			var actualReq container.ExecStartRequest
			client, err := New(
				WithMockClient(func(req *http.Request) (*http.Response, error) {
					if tc.expErr != "" {
						return nil, errors.New("should not have made API request")
					}
					if err := json.NewDecoder(req.Body).Decode(&actualReq); err != nil {
						return nil, err
					}

					return mockJSONResponse(http.StatusOK, nil, ExecStartResult{})(req)
				}),
			)
			assert.NilError(t, err)

			_, err = client.ExecStart(t.Context(), "exec_id", tc.options)
			if tc.expErr != "" {
				assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
				assert.Check(t, is.ErrorContains(err, tc.expErr))
				assert.Check(t, is.DeepEqual(actualReq, tc.expReq))
			} else {
				assert.NilError(t, err)
				assert.Check(t, is.DeepEqual(actualReq, tc.expReq))
			}
		})
	}
}

func TestExecInspectError(t *testing.T) {
	client, err := New(
		WithMockClient(errorMock(http.StatusInternalServerError, "Server error")),
	)
	assert.NilError(t, err)

	_, err = client.ExecInspect(t.Context(), "nothing", ExecInspectOptions{})
	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}

func TestExecInspect(t *testing.T) {
	const expectedURL = "/exec/exec_id/json"
	client, err := New(
		WithMockClient(func(req *http.Request) (*http.Response, error) {
			if err := assertRequest(req, http.MethodGet, expectedURL); err != nil {
				return nil, err
			}
			return mockJSONResponse(http.StatusOK, nil, container.ExecInspectResponse{
				ID:          "exec_id",
				ContainerID: "container_id",
			})(req)
		}),
	)
	assert.NilError(t, err)

	inspect, err := client.ExecInspect(t.Context(), "exec_id", ExecInspectOptions{})
	assert.NilError(t, err)
	assert.Check(t, is.Equal(inspect.ID, "exec_id"))
	assert.Check(t, is.Equal(inspect.ContainerID, "container_id"))
}