package kommandant

import (
	"context"
	"bufio"
	"bytes"
	"reflect"
	"strings"
	"testing"
	//"fmt"
)

// Custom IO shim for tests. We need the features of a bufio and a ReadCloser.
type ioShim struct {
	input      *bufio.Reader
	output     *bufio.Writer
	out_buffer *bytes.Buffer
}

func NewIOShim(initialInput string) (shim *ioShim) {
	shim = &ioShim{}
	shim.input = bufio.NewReader(strings.NewReader(initialInput))
	shim.out_buffer = new(bytes.Buffer)
	shim.output = bufio.NewWriter(shim.out_buffer)
	return shim
}

func (i *ioShim) Read(p []byte) (n int, err error) {
	return i.input.Read(p)
}

func (i *ioShim) Close() (err error) {
	return nil
}

func (i *ioShim) Write(p []byte) (n int, err error) {
	return i.output.Write(p)
}

func (i *ioShim) DumpOutput() (output string) {
	i.output.Flush()
	return i.out_buffer.String()
}

// Helper for testing
type testHelp struct {
	core *Kmdt
	// Special handlers
	PreLoopCount        int
	PostLoopCount       int
	PreCommandCount     int
	PostCommandCount    int
	EmptyLineCount      int
	DefaultCommandCalls []string
	EOFCount            int
	// Custom handlers
	FooCalls []string
	BarCalls []string
}

// Create New_testHelp
func New_testHelp() (t *testHelp) {
	t = &testHelp{}
	t.DefaultCommandCalls = make([]string, 0)
	t.FooCalls = make([]string, 0)
	t.BarCalls = make([]string, 0)
	return t
}

func (t *testHelp) SetCore(k *Kmdt) {
	t.core = k
}

func (t *testHelp) DoEOF(lineIn string) (stopOut bool) {
	t.EOFCount++
	return true
}

func (t *testHelp) DoFoo(lineIn string) (stopOut bool) {
	t.FooCalls = append(t.FooCalls, lineIn)
	return false
}

func (t *testHelp) DoBar(lineIn string) (stopOut bool) {
	t.BarCalls = append(t.BarCalls, lineIn)
	return false
}

func (t *testHelp) HelpBar() {
	t.core.WriteString("Bar of Help\n")
}

func (t *testHelp) HelpQuux() {
}

func (t *testHelp) PreLoop() {
	t.PreLoopCount++
}

func (t *testHelp) PostLoop() {
	t.PostLoopCount++
}

func (t *testHelp) PreCmd(lineIn string) (lineOut string) {
	t.PreCommandCount++
	return lineIn
}

func (t *testHelp) PostCmd(stopIn bool, lineIn string) (stopOut bool) {
	t.PostCommandCount++
	return stopIn
}

func (t *testHelp) EmptyLine(ctx context.Context) (stopOut bool) {
	t.EmptyLineCount++
	return false
}

func (t *testHelp) Default(lineIn string) (stopOut bool) {
	t.DefaultCommandCalls = append(t.DefaultCommandCalls, lineIn)
	return false
}

// Tests
func TestNewBasicKommandant(t *testing.T) {
	// Test basic
	core := NewKommandant(&testHelp{})
	if reflect.DeepEqual(core.CommandQueue, []string{}) == false {
		t.Fatal("TestNewKommandant: incorrect CommandQueue", core.CommandQueue)
	}
	if core.stdin == nil {
		t.Fatal("TestNewKommandant: incorrect default Stdin", core.stdin)
	}
	if core.stdout == nil {
		t.Fatal("TestNewKommandant: incorrect default Stdout", core.stdout)
	}
	if core.IdentChars != "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQUSTUVWXYZ0123456789_" {
		t.Fatal("TestNewKommandant: incorrect IdentChars", core.IdentChars)
	}
	// Test non-default
	shim := NewIOShim("")
	core = NewKommandant(&testHelp{})
	core.SetStdin(shim)
	core.SetStdout(shim)
	if reflect.DeepEqual(core.CommandQueue, []string{}) == false {
		t.Fatal("TestNewKommandant: incorrect CommandQueue", core.CommandQueue)
	}
	if core.IdentChars != "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQUSTUVWXYZ0123456789_" {
		t.Fatal("TestNewKommandant: incorrect IdentChars", core.IdentChars)
	}
}

func TestCmdLoop(t *testing.T) {
	// Create dummy I/O
	shim := NewIOShim("asdf bar\nquux\n")
	th := New_testHelp()
	core := NewKommandant(th)
	core.SetStdin(shim)
	core.SetStdout(shim)
	// Add items to command queue
	core.CommandQueue = []string{"baz 1234"}
	// Test
	core.CmdLoop(context.TODO(), "Testing")
	if th.PreLoopCount != 1 || th.PostLoopCount != 1 ||
		th.PreCommandCount != 4 || th.PostCommandCount != 4 ||
		th.EOFCount != 1 {
		t.Fatal("CmdLoop failure, incorrect call counts", th.PreLoopCount,
			th.PostLoopCount, th.PreCommandCount, th.PostCommandCount,
			th.EOFCount)
	}
	out := shim.DumpOutput()
	if out != "Testing\n(Kmdt) (Kmdt) (Kmdt) " {
		t.Fatal("CmdLoop failure, incorrect output", out)
	}
}

func TestParseLine(t *testing.T) {
	core := NewKommandant(&testHelp{})
	// Test empty line
	cmd, arg, line := core.ParseLine("")
	if cmd != "" {
		t.Fatal("TestParseLine: incorrect cmd", cmd)
	}
	if arg != "" {
		t.Fatal("TestParseLine: incorrect arg", arg)
	}
	if line != "" {
		t.Fatal("TestParseLine: incorrect line", line)
	}
	// Test ordinary command
	cmd, arg, line = core.ParseLine("foo 23")
	if cmd != "foo" {
		t.Fatal("TestParseLine: incorrect cmd", cmd)
	}
	if arg != "23" {
		t.Fatal("TestParseLine: incorrect arg", arg)
	}
	if line != "foo 23" {
		t.Fatal("TestParseLine: incorrect line", line)
	}
	// Test help shortcut
	cmd, arg, line = core.ParseLine("? foo")
	if cmd != "help" {
		t.Fatal("TestParseLine: incorrect cmd", cmd)
	}
	if arg != "foo" {
		t.Fatal("TestParseLine: incorrect arg", arg)
	}
	if line != "help  foo" {
		t.Fatal("TestParseLine: incorrect line", line)
	}
	// Test shell shortcut, no command
	cmd, arg, line = core.ParseLine("! foo")
	if cmd != "" {
		t.Fatal("TestParseLine: incorrect cmd", cmd)
	}
	if arg != "" {
		t.Fatal("TestParseLine: incorrect arg", arg)
	}
	if line != "! foo" {
		t.Fatal("TestParseLine: incorrect line", line)
	}
	// Test shell shortcut, with command
	core.handlers["shell"] = nil
	cmd, arg, line = core.ParseLine("! foo")
	if cmd != "shell" {
		t.Fatal("TestParseLine: incorrect cmd", cmd)
	}
	if arg != "foo" {
		t.Fatal("TestParseLine: incorrect arg", arg)
	}
	if line != "shell  foo" {
		t.Fatal("TestParseLine: incorrect line", line)
	}
}

func TestOneCmd(t *testing.T) {
	// ---------- Setup ----------
	shim := NewIOShim("")
	th := New_testHelp()
	core := NewKommandant(th)
	core.SetStdin(shim)
	core.SetStdout(shim)
	// ---------- Tests ----------
	// Test empty line
	if core.OneCmd(context.TODO(), "\n") != false {
		t.Fatal("OneCmd failure: quit on empty line")
	}
	if th.EmptyLineCount != 1 {
		t.Fatal("OneCmd failure: did not trigger EmptyLine")
	}
	// Test no command
	if core.OneCmd(context.TODO(), "! blah") != false {
		t.Fatal("OneCmd failure: quit on no command")
	}
	if reflect.DeepEqual(th.DefaultCommandCalls, &[]string{"! blah"}) {
		t.Fatal("OneCmd failure, failed default:", th.DefaultCommandCalls)
	}

	// Test EOF
	if core.OneCmd(context.TODO(), "EOF") != true {
		t.Fatal("OneCmd failure: quit on EOF")
	}
	if core.LastCmd != "" {
		t.Fatal("OneCmd failure, LastCmd not cleared on EOF:", core.LastCmd)
	}

	// Test empty command
	th.DefaultCommandCalls = make([]string, 0)
	if core.OneCmd(context.TODO(), "&$*#@ foo bar") != false {
		t.Fatal("OneCmd failure: quit on empty command")
	}
	if core.LastCmd != "&$*#@ foo bar" ||
		reflect.DeepEqual(th.DefaultCommandCalls, &[]string{"&$*#@ foo bar"}) {
		t.Fatal("OneCmd failure:", core.LastCmd, th.DefaultCommandCalls)
	}

	// Test handled
	if core.OneCmd(context.TODO(), "foo bar baz") != false {
		t.Fatal("OneCmd failure: quit on handled")
	}
	if core.LastCmd != "foo bar baz" ||
		reflect.DeepEqual(th.FooCalls, "bar baz") {
		t.Fatal("OneCmd handler failure:", core.LastCmd, th.FooCalls)
	}

	// Test not handled
	th.DefaultCommandCalls = make([]string, 0)
	if core.OneCmd(context.TODO(), "quux bar baz") != false {
		t.Fatal("OneCmd failure: quit on non-handled")
	}
	if core.LastCmd != "quux bar baz" ||
		reflect.DeepEqual(th.DefaultCommandCalls, &[]string{"quux bar baz"}) {
		t.Fatal("OneCmd non-handled failure:", core.LastCmd,
			th.DefaultCommandCalls)
	}
}

func TestDoHelp(t *testing.T) {
	shim := NewIOShim("")
	th := New_testHelp()
	core := NewKommandant(th)
	core.SetStdout(shim)
	// Test help list
	core.DocLeader = "lead"
	core.DoHelp(context.TODO(), "")
	output := shim.DumpOutput()
	if output != "lead\nDocumented commands (type help <topic>):\n========================================\nbar\n\nMiscellaneous help topics:\n==========================\nquux\n\nUndocumented commands:\n======================\nfoo\n\n" {
		t.Fatal("DoHelp failure:", output)
	}
	shim.out_buffer.Reset()
	// Test help on item
	core.DoHelp(context.TODO(), "bar")
	output = shim.DumpOutput()
	if output != "Bar of Help\n" {
		t.Fatal("DoHelp failure:", output)
	}
}

func TestPrintTopics(t *testing.T) {
	shim := NewIOShim("")
	core := NewKommandant(&testHelp{})
	core.SetStdout(shim)
	core.printTopics("Foo", []string{"bar", "baz", "quux"}, 80)
	output := shim.DumpOutput()
	if output != "Foo\n===\nbar  baz  quux\n\n" {
		t.Fatal("printTopics failure:", output)
	}
}

func TestColumnize(t *testing.T) {
	// Test empty list
	formatted := columnize(nil, 0)
	if formatted != "<empty>\n" {
		t.Fatal("TestColumnize: empty list handling failure.", formatted)
	}
	// Test single item
	formatted = columnize([]string{"foo"}, 40)
	if formatted != "foo\n" {
		t.Fatal("TestColumnize: one item failure:", "'"+formatted+"'")
	}
	// Test single row
	formatted = columnize([]string{"foo", "bar", "baz"}, 40)
	if formatted != "foo  bar  baz\n" {
		t.Fatal("TestColumnize: one liner failure", "'"+formatted+"'")
	}
	// Test multi row
	formatted = columnize(
		[]string{"foo", "bar", "baz", "quux", "qwerty", "penguin", "parrot"},
		20,
	)
	if formatted != "foo  quux     parrot\nbar  qwerty\nbaz  penguin\n" {
		t.Fatal("TestColumnize: multi line failure\n", "'"+formatted+"'")
	}
	// Test nrows = size
	formatted = columnize(
		[]string{"foo", "bar", "baz", "quux", "qwerty", "penguin", "parrot"},
		5,
	)
	if formatted != "foo\nbar\nbaz\nquux\nqwerty\npenguin\nparrot\n" {
		t.Fatal("TestColumnize: single column failure\n", "'"+formatted+"'")
	}
}

func TestCalculateColumns(t *testing.T) {
	// Test single row
	nrows, colwidths := calculateColumns([]string{"foo", "bar", "baz"}, 40)
	if nrows != 1 {
		t.Fatal("TestCalculateColumns: incorrect nrows; expected 1, got",
			nrows)
	}
	if reflect.DeepEqual([]int{3, 3, 3}, colwidths) != true {
		t.Fatal("TestCalculateColumns: incorrect colwidths; expected (3, 3, 3), got", colwidths)
	}
	// Test multi row
	nrows, colwidths = calculateColumns(
		[]string{"foo", "bar", "baz", "quux", "qwerty", "penguin", "parrot"},
		20,
	)
	if nrows != 3 {
		t.Fatal("TestCalculateColumns: incorrect nrows; expected 3, got",
			nrows)
	}
	if reflect.DeepEqual([]int{3, 7, 6}, colwidths) != true {
		t.Fatal("TestCalculateColumns: incorrect colwidths; expected (3, 7, 6), got", colwidths)
	}
	// Test nrows = size
	nrows, colwidths = calculateColumns(
		[]string{"foo", "bar", "baz", "quux", "qwerty", "penguin", "parrot"},
		5,
	)
	if nrows != 7 {
		t.Fatal("TestCalculateColumns: incorrect nrows; expected 7, got",
			nrows)
	}
	if reflect.DeepEqual([]int{0}, colwidths) != true {
		t.Fatal("TestCalculateColumns: incorrect colwidths; expected (0), got", colwidths)
	}
}

func TestFormatColumns(t *testing.T) {
	// Test single row
	formatted := formatColumns([]string{"foo", "bar", "baz"}, 1,
		[]int{3, 3, 3})
	if formatted != "foo  bar  baz\n" {
		t.Fatal("TestFormatColumns: one liner failure", "'"+formatted+"'")
	}
	// Test multi row
	formatted = formatColumns(
		[]string{"foo", "bar", "baz", "quux", "qwerty", "penguin", "parrot"},
		3,
		[]int{3, 7, 6},
	)
	if formatted != "foo  quux     parrot\nbar  qwerty\nbaz  penguin\n" {
		t.Fatal("TestFormatColumns: multi line failure\n", "'"+formatted+"'")
	}
	// Test nrows = size
	formatted = formatColumns(
		[]string{"foo", "bar", "baz", "quux", "qwerty", "penguin", "parrot"},
		7,
		[]int{0},
	)
	if formatted != "foo\nbar\nbaz\nquux\nqwerty\npenguin\nparrot\n" {
		t.Fatal("TestFormatColumns: single column failure\n", "'"+formatted+"'")
	}
}

func Test_buildCompletion(t *testing.T) {
	// ---------- Setup ----------
	th := New_testHelp()
	core := NewKommandant(th)
	err := core.EnableReadline(true)
	if err != nil {
		t.Fatal("buildCompletion failure:", err)
	}
	// Check that buildCompletion left behind what it should have
	if core.rootCompleter == nil {
		t.Fatal("buildCompletion failure: nil rootCompleter")
	}
	firstLevel := core.rootCompleter.GetChildren()
	if len(firstLevel) != 4 {
		t.Fatal("buildCompletion failure:", firstLevel)
	}
}
