// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package ui

import (
	"bytes"
	"fmt"
	"io"
	"testing"

	"github.com/hashicorp/nomad/ci"
	"github.com/mitchellh/cli"
	"github.com/shoenig/test/must"
)

func TestWriterUI_Implements(t *testing.T) {
	ci.Parallel(t)
	var _ cli.Ui = new(WriterUI)
}

type writerUITestCase struct {
	name   string                  // the testcase name
	baseUi cli.Ui                  // cli.Ui with accessible writers (currently basicUi or mockUi)
	ui     cli.Ui                  // the full ui object chain (should end in baseUi)
	initFn func(*writerUITestCase) // sets up the struct for the testcase
	ow     *bytes.Buffer           // handle to basicUi's Output writer
	ew     *bytes.Buffer           // handle to basicUi's Error writer
}

func TestWriterUI_OutputWriter(t *testing.T) {
	ci.Parallel(t)

	tcs := []writerUITestCase{
		{
			name: "mockUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = tc.baseUi
			},
		},
		{
			name: "mockUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "mockUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
		{
			name: "basicUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = tc.baseUi
			},
		},
		{
			name: "basicUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "basicUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
	}

	for _, tc := range tcs {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			ci.Parallel(t)
			tc.initFn(&tc)

			wUI, err := NewWriterUI(tc.ui)
			must.NoError(t, err)
			fmt.Fprintf(wUI.OutputWriter(), "foobar")

			switch bui := tc.baseUi.(type) {
			case *cli.MockUi:
				must.Eq(t, "foobar", bui.OutputWriter.String())
				must.Eq(t, "", bui.ErrorWriter.String())
			case *cli.BasicUi:
				must.Eq(t, "foobar", tc.ow.String())
				must.Eq(t, "", tc.ew.String())
			default:
				t.Fatal("invalid base cli.Ui type")
			}
		})
	}
}

func TestWriterUI_Output(t *testing.T) {
	ci.Parallel(t)

	tcs := []writerUITestCase{
		{
			name: "mockUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = tc.baseUi
			},
		},
		{
			name: "mockUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "mockUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
		{
			name: "basicUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = tc.baseUi
			},
		},
		{
			name: "basicUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "basicUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
	}

	for _, tc := range tcs {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			ci.Parallel(t)
			tc.initFn(&tc)

			wUI, err := NewWriterUI(tc.ui)
			must.NoError(t, err)
			wUI.Output("foobar")

			var ov, ev string
			switch bui := tc.baseUi.(type) {
			case *cli.MockUi:
				ov = bui.OutputWriter.String()
				ev = bui.ErrorWriter.String()
			case *cli.BasicUi:
				ov = tc.ow.String()
				ev = tc.ew.String()
			default:
				t.Fatal("invalid base cli.Ui type")
			}

			must.Eq(t, "foobar\n", ov)
			must.Eq(t, "", ev)
		})
	}
}

func TestWriterUI_Info(t *testing.T) {
	ci.Parallel(t)

	tcs := []writerUITestCase{
		{
			name: "mockUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = tc.baseUi
			},
		},
		{
			name: "mockUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "mockUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
		{
			name: "basicUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = tc.baseUi
			},
		},
		{
			name: "basicUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "basicUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
	}

	for _, tc := range tcs {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			ci.Parallel(t)
			tc.initFn(&tc)

			wUI, err := NewWriterUI(tc.ui)
			must.NoError(t, err)
			wUI.Info("INFO")

			var ov, ev string
			switch bui := tc.baseUi.(type) {
			case *cli.MockUi:
				ov = bui.OutputWriter.String()
				ev = bui.ErrorWriter.String()
			case *cli.BasicUi:
				ov = tc.ow.String()
				ev = tc.ew.String()
			default:
				t.Fatal("invalid base cli.Ui type")
			}

			must.Eq(t, "INFO\n", ov)
			must.Eq(t, "", ev)
		})
	}
}

func TestWriterUI_Warn(t *testing.T) {
	ci.Parallel(t)

	tcs := []writerUITestCase{
		{
			name: "mockUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = tc.baseUi
			},
		},
		{
			name: "mockUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "mockUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
		{
			name: "basicUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = tc.baseUi
			},
		},
		{
			name: "basicUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "basicUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
	}

	for _, tc := range tcs {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			ci.Parallel(t)
			tc.initFn(&tc)

			wUI, err := NewWriterUI(tc.ui)
			must.NoError(t, err)
			wUI.Warn("WARN")

			const expected = "WARN\n"

			var ov, ev string
			switch bui := tc.baseUi.(type) {
			case *cli.MockUi:
				ov = bui.OutputWriter.String()
				ev = bui.ErrorWriter.String()
			case *cli.BasicUi:
				ov = tc.ow.String()
				ev = tc.ew.String()
			default:
				t.Fatal("invalid base cli.Ui type")
			}

			must.Eq(t, "", ov)
			must.Eq(t, "WARN\n", ev)
		})
	}
}

func TestWriterUI_Error(t *testing.T) {
	ci.Parallel(t)

	tcs := []writerUITestCase{
		{
			name: "mockUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = tc.baseUi
			},
		},
		{
			name: "mockUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "mockUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.baseUi = cli.NewMockUi()
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
		{
			name: "basicUi/simple",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = tc.baseUi
			},
		},
		{
			name: "basicUi/nested_once",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: tc.baseUi}
			},
		},
		{
			name: "basicUi/nested_twice",
			initFn: func(tc *writerUITestCase) {
				tc.ow = new(bytes.Buffer)
				tc.ew = new(bytes.Buffer)
				tc.baseUi = &cli.BasicUi{Writer: tc.ow, ErrorWriter: tc.ew}
				tc.ui = &cli.ColoredUi{Ui: &cli.ColoredUi{Ui: tc.baseUi}}
			},
		},
	}

	for _, tc := range tcs {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			ci.Parallel(t)
			tc.initFn(&tc)

			wUI, err := NewWriterUI(tc.ui)
			must.NoError(t, err)
			wUI.Warn("ERROR")

			var ov, ev string
			switch bui := tc.baseUi.(type) {
			case *cli.MockUi:
				ov = bui.OutputWriter.String()
				ev = bui.ErrorWriter.String()
			case *cli.BasicUi:
				ov = tc.ow.String()
				ev = tc.ew.String()
			default:
				t.Fatal("invalid base cli.Ui type")
			}

			must.Eq(t, "", ov)
			must.Eq(t, "ERROR\n", ev)
		})
	}
}

func TestWriterUI_Ask(t *testing.T) {
	ci.Parallel(t)

	testCases := []struct {
		name           string
		query          string
		input          string
		expectedQuery  string
		expectedResult string
	}{
		{
			name:           "EmptyString",
			query:          "Middle Name?",
			input:          "\n",
			expectedQuery:  "Middle Name? ",
			expectedResult: "",
		},
		{
			name:           "NonEmptyString",
			query:          "Name?",
			input:          "foo bar\nbaz\n",
			expectedQuery:  "Name? ",
			expectedResult: "foo bar",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			ci.Parallel(t)

			inReader, inWriter := io.Pipe()
			t.Cleanup(func() {
				inReader.Close()
				inWriter.Close()
			})

			writer := new(bytes.Buffer)

			WriterUI, err := NewWriterUI(&cli.BasicUi{
				Reader: inReader,
				Writer: writer,
			})
			must.NoError(t, err)

			go inWriter.Write([]byte(tc.input))

			result, err := WriterUI.Ask(tc.query)
			must.NoError(t, err)
			must.Eq(t, writer.String(), tc.expectedQuery)
			must.Eq(t, result, tc.expectedResult)
		})
	}
}

func TestWriterUI_AskSecret(t *testing.T) {
	ci.Parallel(t)

	inReader, inWriter := io.Pipe()
	t.Cleanup(func() {
		inReader.Close()
		inWriter.Close()
	})

	writer := new(bytes.Buffer)
	wUI, err := NewWriterUI(&cli.BasicUi{
		Reader: inReader,
		Writer: writer,
	})
	must.NoError(t, err)

	go inWriter.Write([]byte("foo bar\nbaz\n"))

	result, err := wUI.AskSecret("Name?")
	must.NoError(t, err)
	must.Eq(t, writer.String(), "Name? ")
	must.Eq(t, result, "foo bar")
}
