package door

// Need net, flag for setupSockets
import (
	"flag"
	"fmt"
	"log"
	"net"
	"os"
	"strings"
	"testing"
	"time"
)

// Should tests not delete the logfiles?
// To make use of this, set KEEP_LOGS bool = true and:
// go test -c; ./door.test -test.v -test.timeout 40s
// Running just one test:
// go test -c; ./door.test -test.run DoorCP437 -test.v

const KEEP_LOGS bool = true     // false
const VERBOSE_TEST bool = false // true // false

func TestGoto(t *testing.T) {
	GotoMap := map[string][]int{"\x1b[10;20H": {20, 10},
		"\x1b[20;10H":  {10, 20},
		"\x1b[80;120H": {120, 80},
		"\x1b[1;1H":    {1, 1},
	}

	for text, code := range GotoMap {
		gt := string(Goto(code[0], code[1]))
		if text != gt {
			t.Errorf("Goto: Expected %#v (%#v), got %#v", text, code, gt)
		}
	}
}

func TestLogfileFailure(t *testing.T) {
	tmpFile, err := os.CreateTemp("", "test-door32.sys-*")
	if err != nil {
		panic("Cannot create temporary file")
	}

	defer func() {
		if r := recover(); r == nil {
			t.Error("Init did not panic on logfile error.")
		}
	}()

	// Remember to clean up the file afterwards
	defer os.Remove(tmpFile.Name())

	// This test should fail before we even need sockets

	var fd int = 0

	// Create door32.sys file
	dfc := DropfileConfig{2, fd, "Test BBSID", 1701, "Real Username", "Handle", 880, 28, 13}

	err = CreateDoor32File(&dfc, tmpFile)
	if err != nil {
		t.Error("tmpFile.WriteString:", err)
	}
	err = tmpFile.Close()
	if err != nil {
		t.Error("tmpFile.Close:", err)
	}

	d := Door{}

	// If I call d.Init() more then once flag complains about flag redefined.
	// Reset flags
	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
	// preset commandline args so door can init
	os.Args = []string{"door", "-d", tmpFile.Name()}

	uid := os.Getuid()

	if uid == 0 {
		// Running as root.  Use invalid path here.
		d.Init("/invalid-path/badpath-test")
	} else {
		// uid == -1 (windows), this should also fail there.
		d.Init("/badname-test")
	}
}

func TestDetectFail(t *testing.T) {
	tmpFile, err := os.CreateTemp("", "test-door32.sys-*")
	if err != nil {
		panic("Cannot create temporary file")
	}

	// Remember to clean up the file afterwards

	defer os.Remove(tmpFile.Name())

	// establish network socket connection to set Comm_handle
	server, client := setupSockets()

	// We're not testing closed connections, so:
	defer server.Close()
	defer client.Close()

	// Send nothing
	var fd int = socket_to_fd(client)
	defer close_fd(fd)

	// Create door32.sys file
	dfc := DropfileConfig{2, fd, "Test BBSID", 1701, "Real Username", "Handle", 880, 28, 13}

	err = CreateDoor32File(&dfc, tmpFile)
	if err != nil {
		t.Error("tmpFile.WriteString:", err)
	}
	err = tmpFile.Close()
	if err != nil {
		t.Error("tmpFile.Close:", err)
	}

	d := Door{} // ReaderCanClose: true}

	// Because we're not the only one calling door.Init(), the
	// door global variables might be from a previous test run.

	Unicode = false
	CP437 = false
	Full_CP437 = false
	Width = 0
	Height = 0

	// If I call d.Init() more then once flag complains about flag redefined.
	// Reset flags
	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
	// preset commandline args so door can init
	os.Args = []string{"door", "-d", tmpFile.Name()}
	d.Init("menu-test")

	t.Logf("Init completed.")
	defer d.Close()
	// clean up log file
	// I don't need to.  Tests are run in /tmp/go-buildNNNN.
	// defer os.Remove("menu-test-13.log")

	if Unicode || CP437 {
		t.Errorf("Expected FALSE, got Unicode %t CP437 %t", Unicode, CP437)
	}

	if Width != 0 || Height != 0 {
		t.Errorf("Expected 0, 0, got Width %d, Height %d", Width, Height)
	}

	t.Logf("Closing server and client...")
	server.Close()
	client.Close()

	// time.Sleep(time.Duration(1) * time.Second)
	t.Logf("Done.")
}

// Pass d*Door to test.  Otherwise we get a copy of the object passed!
// go vet (shows this error/issue)

// Generic tests that will be called from CP437 and Unicode routines
func InputTests(t *testing.T, server net.Conn, d *Door, mode string) {
	var err error
	var buffer string

	keytest := map[string][]rune{
		"\x1b":             []rune{0x1b},
		"\x0d\x00":         []rune{0x0d},
		"\x0d\x0a":         []rune{0x0d},
		"\x0dCQ":           []rune{0x0d, 'C', 'Q'},
		"\x0dA":            []rune{0x0d, 'A'},
		"\x0dCAT":          []rune{0x0d, 'C', 'A', 'T'},
		"\U0001f920\u2615": []rune{'\U0001f920', '\u2615'},
	}

	/*
		Test input buffer processing.
		Create a string with unicode that doesn't fit into the
		entire read buffer.  And build the proper response to match.
	*/

	// x = number of missing bytes (from unicode symbol).
	for x := 1; x <= 3; x++ {
		tosend := strings.Repeat(" ", READ_SIZE-x) + "\U0001f920"
		receive := []rune(strings.Repeat(" ", READ_SIZE-x))
		receive = append(receive, '\U0001f920')
		keytest[tosend] = receive
	}

	keyextest := map[string][]Extended{
		"\x00\x50\x00\x48\x00\x4b\x00\x4d": []Extended{DOWN_ARROW, UP_ARROW, LEFT_ARROW, RIGHT_ARROW},
		"\x00\x47\x00\x4f\x00\x49\x00\x51": []Extended{HOME, END, PAGE_UP, PAGE_DOWN},
		"\x00\x3b\x00\x3c\x00\x3d\x00\x3e": []Extended{F1, F2, F3, F4},
		"\x00\x3f\x00\x40\x00\x41\x00\x42": []Extended{F5, F6, F7, F8},
		"\x00\x43\x00\x44\x00\x52\x00\x53": []Extended{F9, F10, INSERT, DELETE},
		"\x00\x45\x00\x46":                 []Extended{F11, F12},
		"\x1b[A\x1b[B\x1b[C\x1b[D":         []Extended{UP_ARROW, DOWN_ARROW, RIGHT_ARROW, LEFT_ARROW},
		"\x1b[H\x1b[F\x1b[K\x1b[V\x1b[U":   []Extended{HOME, END, END, PAGE_UP, PAGE_DOWN},
		"\x1b[5~\x1b[6~":                   []Extended{PAGE_UP, PAGE_DOWN},
		"\x1b[@\x1b[2~\x1b[3~":             []Extended{INSERT, INSERT, DELETE},
		"\x1b[1~\x1b[4~":                   []Extended{HOME, END},
		"\x1bOP\x1bOQ\x1bOR\x1bOS":         []Extended{F1, F2, F3, F4},
		"\x1b[15~\x1b[17~\x1b[18~\x1b[19~": []Extended{F5, F6, F7, F8},
		"\x1b[20~\x1b[21~\x1b[23~\x1b[24~": []Extended{F9, F10, F11, F12},
	}

	// 50ms causes 3 tries (internal is 200ms, use 210ms).
	keyWait := time.Duration(210 * time.Millisecond)

	// Verify input is empty
	for {
		input, ex, err := d.WaitKey(keyWait)
		if err == nil {
			t.Errorf("Keys: %#v, %#v\n", input, ex)
		} else {
			break
		}
	}

	t.Logf("Starting keytest (%s)\n", mode)

	for send, get := range keytest {
		if VERBOSE_TEST {
			t.Logf("Sending %#v: Expect: %#v\n", send, get)
		}
		var buffer []byte = []byte(send)
		_, err = server.Write(buffer)
		if err != nil {
			t.Error("server.Write:", err)
		}
		time.Sleep(time.Millisecond)

		var recv []rune = make([]rune, 0)
		var retries int = 0
		for {
			input, _, err := d.WaitKey(keyWait)
			if VERBOSE_TEST {
				t.Logf(">> %#v, %#v\n", input, err)
			}

			if err == nil {
				recv = append(recv, input)
				if len(recv) != len(get) {
					continue
				}
				break
			} else {
				t.Logf("WaitKey Err: %#v\n", err)
				if len(recv) != len(get) {
					if retries < 5 {
						retries++
						t.Logf("Retry %d want %d got %d\n", retries, len(get), len(recv))
						continue
					}
					break
				}
			}
		}

		if len(recv) != len(get) {
			t.Errorf("FAILURE: Sent %#v, LEN expected %#v, got %#v", send, get, recv)
		} else {
			matches := true
			for idx, i := range get {
				if recv[idx] != i {
					matches = false
					break
				}
			}
			if !matches {
				t.Errorf("FAILURE: Sent %#v, MATCH expected %#v, got %#v", send, get, recv)
			} else {
				if VERBOSE_TEST {
					t.Logf("Success.")
				}
			}
		}
	}

	t.Logf("Starting keyextest (%s)\n", mode)

	for send, get := range keyextest {
		if VERBOSE_TEST {
			t.Logf("Sending %#v: Expect: %#v\n", send, get)
		}
		var buffer []byte = []byte(send)
		_, err = server.Write(buffer)
		if err != nil {
			t.Error("server.Write:", err)
		}
		time.Sleep(time.Millisecond)

		var recv []Extended = make([]Extended, 0)
		var retries int = 0
		for {
			_, ex, err := d.WaitKey(keyWait)

			if err == nil {
				recv = append(recv, ex)

				if len(recv) != len(get) {
					continue
				}
				break
			} else {
				t.Logf("WaitKey err: #%v\n", err)
				if len(recv) != len(get) {
					if retries < 5 {
						retries++
						t.Logf("Retry %d want %d got %d\n", retries, len(get), len(recv))
						continue
					}

					break

				}
			}
		}

		if len(recv) != len(get) {
			t.Errorf("Sent %#v, LEN expected %#v, got %#v", send, get, recv)
		} else {
			matches := true
			for idx, i := range get {
				if recv[idx] != i {
					matches = false
					break
				}
			}
			if !matches {
				t.Errorf("Sent %#v, MATCH expected %#v, got %#v", send, get, recv)
			}
		}
	}

	clear_socket(server, t)

	t.Logf("Starting WaitKey timeout (%s)...\n", mode)
	_, _, timeout := d.WaitKey(keyWait)
	if timeout == nil {
		t.Errorf("Expected timeout, got %d / %X", timeout, timeout)
	} else {
		t.Logf("Ok! Buffer should be empty!  -1 (timeout)")
	}

	t.Logf("Starting input test (%s)\n", mode)
	// Input test
	buffer = "1234567890\r"
	_, err = server.Write([]byte(buffer))
	if err != nil {
		t.Error("server.Write:", err)
	}
	time.Sleep(time.Millisecond)

	var input string = d.Input(5)

	if input != "12345" {
		t.Errorf("Expected Input(5) = 12345, but got %#v", input)
	}

	// read everything
	var result string = clear_socket(server, t)

	expected := "     \x08\x08\x08\x08\x0812345\x07\x07\x07\x07"
	if result != expected {
		expected = "     \x08\x08\x08\x08\x0812345\x07\x07\x07\x07\x07"
		if result != expected {
			t.Errorf("Buffer Input(5): Expected %#v, got %#v\n", expected, result)
		}
	}
	err = server.SetReadDeadline(time.Time{})
	if err != nil {
		t.Error("server.SetReadDeadLine:", err)
	}

	buffer = "12345678\x08\x089\r"
	_, err = server.Write([]byte(buffer))
	if err != nil {
		t.Error("server.Write:", err)
	}
	time.Sleep(time.Millisecond)

	input = d.Input(5)

	if input != "1239" {
		t.Errorf("Expected Input(5) = 1239, but got %#v", input)
	}

	buffer = "12\x08\x08\x08987\x00\x48654321\r"
	_, err = server.Write([]byte(buffer))
	if err != nil {
		t.Error("server.Write:", err)
	}
	time.Sleep(time.Millisecond)

	input = d.Input(5)

	if input != "98765" {
		t.Errorf("Expected Input(5) = 98765, but got %#v", input)
	}

	t.Logf("Starting cursor pos test (%s)\n", mode)
	var cpos []CursorPos = []CursorPos{{1, 1}, {50, 5}, {4, 40}, {80, 25}, {160, 55}}
	for _, pos := range cpos {
		if VERBOSE_TEST {
			t.Logf("Sending CursorPos(%d,%d)\n", pos.X, pos.Y)
		}
		buffer = CursorReply(pos)
		_, err := server.Write([]byte(buffer))
		if err != nil {
			t.Error("server.Write:", err)
		}
	}

	for _, pos := range cpos {
		_, ex, err := d.WaitKey(keyWait)
		if err != nil {
			t.Error("WaitKey:", err)
			continue
		}

		if ex != CURSOR {
			t.Errorf("Expected Extended = CURSOR (was %s)", ex.String())
			continue
		}

		cp, ok := d.GetCursorPos()

		if !ok {
			t.Error("Extended sent CURSOR, but GetCursorPos failed.")
			continue
		}

		if (cp.X != pos.X) || (cp.Y != pos.Y) {
			t.Errorf("Expected (%d,%d), Got (%d,%d)", pos.X, pos.Y, cp.X, cp.Y)
		}
	}

	t.Logf("Starting Mouse test (%s)\n", mode)
	var mouse []Mouse = []Mouse{{1, 4, 5}, {4, 4, 5}, {45, 80, 25}, {2, 55, 17}}
	for _, m := range mouse {
		if VERBOSE_TEST {
			t.Logf("Sending Mouse(%d @ %d,%d)\n", m.Button, m.X, m.Y)
		}
		buffer = MouseReply(m)
		_, err = server.Write([]byte(buffer))
		if err != nil {
			t.Error("server.Write:", err)
		}
	}

	for _, pos := range mouse {
		_, ex, err := d.WaitKey(keyWait)
		if err != nil {
			t.Error("WaitKey:", err)
			continue
		}

		if ex != MOUSE {
			t.Errorf("Expected Extended = MOUSE (was %s)", ex.String())
			continue
		}

		mpos, ok := d.GetMouse()

		if !ok {
			t.Error("Extended sent MOUSE, but GetMouse failed.")
			continue
		}

		if (pos.Button != mpos.Button) || (pos.X != mpos.X) || (pos.Y != mpos.Y) {
			t.Errorf("Expected (%d @ %d, %d), Got (%d @ %d, %d)",
				pos.Button, pos.X, pos.Y, mpos.Button, mpos.X, mpos.Y)
		}
	}
}

func TestDoorCP437(t *testing.T) {
	var tmpFile *os.File
	var err error
	// Dropfile
	tmpFile, err = os.CreateTemp("", "test-door32.sys-*")
	if err != nil {
		panic("Cannot create temporary file")
	}

	defer os.Remove(tmpFile.Name())

	// establish network socket connection to set Comm_handle
	var server, client net.Conn
	server, client = setupSockets()

	// Ok, we have a server socket, and the client socket (that the door would talk to)

	// CP437 80x25 response
	buffer := CP437WidthHeight(80, 25)
	_, err = server.Write([]byte(buffer))
	if err != nil {
		t.Error("server.Write:", err)
	}
	time.Sleep(time.Millisecond)

	// Access Fd (File descriptor) of client for dropfile
	var fd int = socket_to_fd(client)
	defer close_fd(fd)

	// Create door32.sys file
	var node int = 10
	dfc := DropfileConfig{2, fd, "Test BBSID", 1701, "Real Username", "Handle", 880, 28, node}

	err = CreateDoor32File(&dfc, tmpFile)
	if err != nil {
		t.Error("tmpFile.WriteString:", err)
	}
	err = tmpFile.Close()
	if err != nil {
		t.Error("tmpFile.Close:", err)
	}

	d := Door{} // Deprecated: ReaderCanClose: true}

	// Because we're not the only one calling door.Init(), the
	// door global variables might be from a previous test run.
	// Reset back to defaults.

	Unicode = false
	CP437 = false
	Full_CP437 = false
	Width = 0
	Height = 0

	// If I call d.Init() more then once flag complains about flag redefined.
	// Reset flags
	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
	// preset commandline args so door can init
	os.Args = []string{"door", "-d", tmpFile.Name()}
	d.Init("input-test")

	defer d.Close()
	// clean up logfile - (filename is from Init name + node #)
	if !KEEP_LOGS {
		defer os.Remove(fmt.Sprintf("input-test-%d.log", node))
	}

	if VERBOSE_TEST {
		t.Logf("Verify Unicode %dX%d\n", 80, 25)
	}

	// Ok!
	if Unicode {
		t.Errorf("Unicode is %t", Unicode)
	}

	if !CP437 {
		t.Errorf("Expect CP437 true, is %t", CP437)
	}

	if Width != 80 {
		t.Errorf("Width not 80: %d", Width)
	}

	if Height != 25 {
		t.Errorf("Height not 25: %d", Height)
	}

	clear_socket(server, t)

	InputTests(t, server, &d, "CP437")

	t.Logf("server close")
	log.Println("server close")

	server.Close()

	time.Sleep(time.Millisecond)
	keyWait := time.Duration(50 * time.Millisecond)

	_, _, err = d.WaitKey(keyWait)
	if err != ErrDisconnected {
		t.Errorf("Expected ErrDisconnected, got %#v", err)
	}

	if !d.Disconnect() {
		t.Errorf("Disconnected flag shows: %t (should be true)", d.Disconnect())
	}

	_, _, err = d.WaitKey(time.Millisecond)

	if err != ErrDisconnected {
		t.Errorf("Expected ErrDisconnected, got %#v", err)
	}

	t.Logf("client close")
	client.Close()
	// close_fd(fd)
	time.Sleep(time.Millisecond)

	t.Logf("Input on closed server and client")
	var blank string = d.Input(5)

	if blank != "" {
		t.Errorf("Input should return blank (hangup).")
	}

	_, _, err = d.WaitKey(time.Millisecond)

	if err != ErrDisconnected {
		t.Errorf("Expected ErrDisconnected, got %#v", err)
	}

	d.Write([]byte("\x00"))
	time.Sleep(time.Millisecond)
}

func TestDoorUnicode(t *testing.T) {
	var tmpFile *os.File
	var err error

	// Dropfile
	tmpFile, err = os.CreateTemp("", "test-door32.sys-*")
	if err != nil {
		panic("Cannot create temporary file")
	}

	defer os.Remove(tmpFile.Name())

	// establish network socket connection to set Comm_handle
	var server, client net.Conn
	server, client = setupSockets()

	// Ok, we have a server socket, and the client socket (that the door would talk to)

	// unicode 190x43 response
	buffer := UnicodeWidthHeight(190, 43)
	_, err = server.Write([]byte(buffer))
	if err != nil {
		t.Error("server.Write:", err)
	}
	time.Sleep(time.Millisecond)

	// Access Fd (File descriptor) of client for dropfile
	var fd int = socket_to_fd(client)
	defer close_fd(fd)

	// Create door32.sys file
	var node int = 11
	dfc := DropfileConfig{2, fd, "Test BBSID", 1701, "Real Username", "Handle", 880, 28, node}

	err = CreateDoor32File(&dfc, tmpFile)
	if err != nil {
		t.Error("tmpFile.WriteString:", err)
	}
	err = tmpFile.Close()
	if err != nil {
		t.Error("tmpFile.Close:", err)
	}

	d := Door{} // Deprecated: ReaderCanClose: true}

	// Because we're not the only one calling door.Init(), the
	// door global variables might be from a previous test run.
	// Reset back to defaults.

	Unicode = false
	CP437 = false
	Full_CP437 = false
	Width = 0
	Height = 0

	// If I call d.Init() more then once flag complains about flag redefined.
	// Reset flags
	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
	// preset commandline args so door can init
	os.Args = []string{"door", "-d", tmpFile.Name()}
	d.Init("input-test")

	defer d.Close()
	// clean up logfile - (filename is from Init name + node #)
	if !KEEP_LOGS {
		defer os.Remove(fmt.Sprintf("input-test-%d.log", node))
	}

	// Ok!
	if !Unicode {
		t.Errorf("Unicode not true %t", Unicode)
	}

	if Width != 190 {
		t.Errorf("Width not 190: %d", Width)
	}

	if Height != 43 {
		t.Errorf("Height not 43: %d", Height)
	}

	clear_socket(server, t)

	keyWait := time.Duration(50 * time.Millisecond)
	InputTests(t, server, &d, "Unicode")

	buffer = "\U0001f9201234\r"
	// double wide (2) + 4 = 6, we're inputting 5.
	_, err = server.Write([]byte(buffer))
	if err != nil {
		t.Error("server.Write:", err)
	}
	time.Sleep(time.Millisecond)

	// Unicode mode can handle unicode input.  CP437 mode can't -- really.
	input := d.Input(5)
	expect := "\U0001f920123"
	if input != expect {
		t.Errorf("Expected Input(5) = %#v, but got %#v", expect, input)
	}

	clear_socket(server, t)

	t.Logf("server close")
	log.Println("server close")

	server.Close()
	time.Sleep(time.Millisecond)

	_, _, err = d.WaitKey(keyWait)
	if err != ErrDisconnected {
		t.Errorf("Expected ErrDisconnected, got %#v", err)
	}

	if !d.Disconnect() {
		t.Errorf("Disconnected flag shows: %t (should be true)", d.Disconnect())
	}

	_, _, err = d.WaitKey(time.Millisecond)

	if err != ErrDisconnected {
		t.Errorf("Expected ErrDisconnected, got %#v", err)
	}

	t.Logf("client close")
	client.Close()
	time.Sleep(time.Millisecond)

	t.Logf("Input on closed server and client")
	var blank string = d.Input(5)

	if blank != "" {
		t.Errorf("Input should return blank (hangup).")
	}

	_, _, err = d.WaitKey(time.Millisecond)

	if err != ErrDisconnected {
		t.Errorf("Expected ErrDisconnected, got %#v", err)
	}

	d.Write([]byte("\x00"))
	time.Sleep(time.Millisecond)
}