package jsonmessage // import "github.com/docker/docker/pkg/jsonmessage"

import (
	"bytes"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/moby/term"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

func TestError(t *testing.T) {
	je := JSONError{404, "Not found"}
	assert.Assert(t, is.Error(&je, "Not found"))
}

func TestProgressString(t *testing.T) {
	type expected struct {
		short string
		long  string
	}

	shortAndLong := func(short, long string) expected {
		return expected{short: short, long: long}
	}

	start := time.Date(2017, 12, 3, 15, 10, 1, 0, time.UTC)
	timeAfter := func(delta time.Duration) func() time.Time {
		return func() time.Time {
			return start.Add(delta)
		}
	}

	testcases := []struct {
		name     string
		progress JSONProgress
		expected expected
	}{
		{
			name: "no progress",
		},
		{
			name:     "progress 1",
			progress: JSONProgress{Current: 1},
			expected: shortAndLong("      1B", "      1B"),
		},
		{
			name: "some progress with a start time",
			progress: JSONProgress{
				Current: 20,
				Total:   100,
				Start:   start.Unix(),
				nowFunc: timeAfter(time.Second),
			},
			expected: shortAndLong(
				"     20B/100B 4s",
				"[==========>                                        ]      20B/100B 4s",
			),
		},
		{
			name:     "some progress without a start time",
			progress: JSONProgress{Current: 50, Total: 100},
			expected: shortAndLong(
				"     50B/100B",
				"[=========================>                         ]      50B/100B",
			),
		},
		{
			name:     "current more than total is not negative gh#7136",
			progress: JSONProgress{Current: 50, Total: 40},
			expected: shortAndLong(
				"     50B",
				"[==================================================>]      50B",
			),
		},
		{
			name:     "with units",
			progress: JSONProgress{Current: 50, Total: 100, Units: "units"},
			expected: shortAndLong(
				"50/100 units",
				"[=========================>                         ] 50/100 units",
			),
		},
		{
			name:     "current more than total with units is not negative ",
			progress: JSONProgress{Current: 50, Total: 40, Units: "units"},
			expected: shortAndLong(
				"50 units",
				"[==================================================>] 50 units",
			),
		},
		{
			name:     "hide counts",
			progress: JSONProgress{Current: 50, Total: 100, HideCounts: true},
			expected: shortAndLong(
				"",
				"[=========================>                         ] ",
			),
		},
	}

	for _, testcase := range testcases {
		t.Run(testcase.name, func(t *testing.T) {
			testcase.progress.winSize = 100
			assert.Equal(t, testcase.progress.String(), testcase.expected.short)

			testcase.progress.winSize = 200
			assert.Equal(t, testcase.progress.String(), testcase.expected.long)
		})
	}
}

func TestJSONMessageDisplay(t *testing.T) {
	now := time.Now()
	messages := map[JSONMessage][]string{
		// Empty
		{}: {"\n", "\n"},
		// Status
		{
			Status: "status",
		}: {
			"status\n",
			"status\n",
		},
		// General
		{
			Time:   now.Unix(),
			ID:     "ID",
			From:   "From",
			Status: "status",
		}: {
			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(RFC3339NanoFixed)),
			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(RFC3339NanoFixed)),
		},
		// General, with nano precision time
		{
			TimeNano: now.UnixNano(),
			ID:       "ID",
			From:     "From",
			Status:   "status",
		}: {
			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
		},
		// General, with both times Nano is preferred
		{
			Time:     now.Unix(),
			TimeNano: now.UnixNano(),
			ID:       "ID",
			From:     "From",
			Status:   "status",
		}: {
			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
		},
		// Stream over status
		{
			Status: "status",
			Stream: "stream",
		}: {
			"stream",
			"stream",
		},
		// With progress message
		{
			Status:          "status",
			ProgressMessage: "progressMessage",
		}: {
			"status progressMessage",
			"status progressMessage",
		},
		// With progress, stream empty
		{
			Status:   "status",
			Stream:   "",
			Progress: &JSONProgress{Current: 1},
		}: {
			"",
			fmt.Sprintf("%c[2K\rstatus       1B\r", 27),
		},
	}

	// The tests :)
	for jsonMessage, expectedMessages := range messages {
		// Without terminal
		data := bytes.NewBuffer([]byte{})
		if err := jsonMessage.Display(data, false); err != nil {
			t.Fatal(err)
		}
		if data.String() != expectedMessages[0] {
			t.Fatalf("Expected %q,got %q", expectedMessages[0], data.String())
		}
		// With terminal
		data = bytes.NewBuffer([]byte{})
		if err := jsonMessage.Display(data, true); err != nil {
			t.Fatal(err)
		}
		if data.String() != expectedMessages[1] {
			t.Fatalf("\nExpected %q\n     got %q", expectedMessages[1], data.String())
		}
	}
}

// Test JSONMessage with an Error. It will return an error with the text as error, not the meaning of the HTTP code.
func TestJSONMessageDisplayWithJSONError(t *testing.T) {
	data := bytes.NewBuffer([]byte{})
	jsonMessage := JSONMessage{Error: &JSONError{404, "Can't find it"}}

	err := jsonMessage.Display(data, true)
	if err == nil || err.Error() != "Can't find it" {
		t.Fatalf("Expected a JSONError 404, got %q", err)
	}

	jsonMessage = JSONMessage{Error: &JSONError{401, "Anything"}}
	err = jsonMessage.Display(data, true)
	assert.Check(t, is.Error(err, "Anything"))
}

func TestDisplayJSONMessagesStreamInvalidJSON(t *testing.T) {
	var inFd uintptr
	data := bytes.NewBuffer([]byte{})
	reader := strings.NewReader("This is not a 'valid' JSON []")
	inFd, _ = term.GetFdInfo(reader)

	exp := "invalid character "
	if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err == nil || !strings.HasPrefix(err.Error(), exp) {
		t.Fatalf("Expected error (%s...), got %q", exp, err)
	}
}

func TestDisplayJSONMessagesStream(t *testing.T) {
	var inFd uintptr

	messages := map[string][]string{
		// empty string
		"": {
			"",
			"",
		},
		// Without progress & ID
		`{ "status": "status" }`: {
			"status\n",
			"status\n",
		},
		// Without progress, with ID
		`{ "id": "ID","status": "status" }`: {
			"ID: status\n",
			"ID: status\n",
		},
		// With progress
		`{ "id": "ID", "status": "status", "progress": "ProgressMessage" }`: {
			"ID: status ProgressMessage",
			fmt.Sprintf("\n%c[%dAID: status ProgressMessage%c[%dB", 27, 1, 27, 1),
		},
		// With progressDetail
		`{ "id": "ID", "status": "status", "progressDetail": { "Current": 1} }`: {
			"", // progressbar is disabled in non-terminal
			fmt.Sprintf("\n%c[%dA%c[2K\rID: status       1B\r%c[%dB", 27, 1, 27, 27, 1),
		},
	}

	// Use $TERM which is unlikely to exist, forcing DisplayJSONMessageStream to
	// (hopefully) use &noTermInfo.
	t.Setenv("TERM", "xyzzy-non-existent-terminfo")

	for jsonMessage, expectedMessages := range messages {
		data := bytes.NewBuffer([]byte{})
		reader := strings.NewReader(jsonMessage)
		inFd, _ = term.GetFdInfo(reader)

		// Without terminal
		if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err != nil {
			t.Fatal(err)
		}
		if data.String() != expectedMessages[0] {
			t.Fatalf("Expected an %q, got %q", expectedMessages[0], data.String())
		}

		// With terminal
		data = bytes.NewBuffer([]byte{})
		reader = strings.NewReader(jsonMessage)
		if err := DisplayJSONMessagesStream(reader, data, inFd, true, nil); err != nil {
			t.Fatal(err)
		}
		if data.String() != expectedMessages[1] {
			t.Fatalf("\nExpected %q\n     got %q", expectedMessages[1], data.String())
		}
	}
}
