package ultrabeam

import (
	"bufio"
	"errors"
	"strings"
	"testing"
	"time"
)

// Stop must not return while the poll loop is still alive: on a serial link the
// loop owns the port, and a client that outlives its Stop is what turned every
// later connection into "Serial port busy".
func TestStopWaitsForPollLoop(t *testing.T) {
	// A transport that cannot connect, so the loop spends its life in open() and
	// the reconnect path — the state the real fault happened in.
	c := New(Transport{Mode: "tcp", Host: "127.0.0.1", Port: 1})
	if err := c.Start(); err != nil {
		t.Fatal(err)
	}
	done := c.done
	time.Sleep(50 * time.Millisecond)
	c.Stop()
	select {
	case <-done:
	default:
		t.Fatal("Stop returned while the poll loop was still running")
	}
	// Stopping twice must not panic on the closed channel.
	c.Stop()
}

func TestPortBusyHint(t *testing.T) {
	// The Windows driver's own words, and the ones an operator has to act on.
	if h := portBusyHint("serial", "COM13", errors.New("Serial port busy")); !strings.Contains(h, "COM13") {
		t.Fatalf("no hint for a busy port: %q", h)
	}
	if h := portBusyHint("serial", "COM13", errors.New("Access is denied.")); h == "" {
		t.Fatal("no hint for access denied")
	}
	// A port that simply is not there is a different problem, and saying "another
	// program has it" would send the operator hunting for a program that is not
	// running.
	if h := portBusyHint("serial", "COM13", errors.New("The system cannot find the file specified.")); h != "" {
		t.Fatalf("hinted at a busy port for a missing one: %q", h)
	}
	if h := portBusyHint("tcp", "", errors.New("connection refused")); h != "" {
		t.Fatalf("serial hint on a TCP link: %q", h)
	}
}

// silentPort answers every read the way a serial port with nothing on the other
// end does: no bytes, no error. bufio turns a run of those into ErrNoProgress.
type silentPort struct{ writes int }

func (s *silentPort) Read(p []byte) (int, error)  { return 0, nil }
func (s *silentPort) Write(p []byte) (int, error) { s.writes++; return len(p), nil }
func (s *silentPort) Close() error                { return nil }

// A controller that never answers must fail ONCE, promptly, with a message that
// says so — not wedge the poll loop for minutes inside bufio's retry budget.
func TestSilentControllerFailsWithinTheReadTimeout(t *testing.T) {
	c := New(Transport{Mode: "serial", COM: "COM_TEST", Baud: 9600})
	c.conn = &silentPort{}
	c.reader = bufio.NewReader(c.conn)

	start := time.Now()
	_, err := c.sendCommand(CMD_STATUS, nil)
	elapsed := time.Since(start)

	if err == nil {
		t.Fatal("a silent controller reported success")
	}
	if elapsed > 3*ubReadTimeout {
		t.Fatalf("took %s to give up — the read deadline is not bounding the exchange", elapsed.Round(time.Millisecond))
	}
	if !strings.Contains(err.Error(), "no reply") {
		t.Fatalf("unhelpful error for a silent port: %v", err)
	}
}
