package steppir

import (
	"errors"
	"io"
	"testing"
	"time"
)

// silentPort is a serial port that has stopped answering: every read times out.
// On Windows that is reported as (0, nil) — a timeout is not an error on this
// transport — which is precisely what io.ReadFull cannot survive.
type silentPort struct{ reads int }

func (p *silentPort) Read(b []byte) (int, error) {
	p.reads++
	time.Sleep(5 * time.Millisecond) // stand in for the port's read timeout
	return 0, nil
}
func (p *silentPort) Write(b []byte) (int, error) { return len(b), nil }
func (p *silentPort) Close() error                { return nil }

// A controller that goes quiet must make the read FAIL, not hang. Hanging held
// the io mutex, so the poll loop never reported a fault and every operator
// command blocked behind it: the antenna stopped responding and the log had
// nothing in it.
func TestReadFrameGivesUpOnASilentController(t *testing.T) {
	done := make(chan error, 1)
	go func() {
		done <- readFrame(&silentPort{}, make([]byte, 11), 100*time.Millisecond)
	}()
	select {
	case err := <-done:
		if err == nil {
			t.Fatal("a silent controller was reported as a good frame")
		}
	case <-time.After(3 * time.Second):
		t.Fatal("readFrame never returned — the driver is wedged exactly as it was in the field")
	}
}

// dribblePort delivers the frame a few bytes at a time, with empty reads in
// between — a slow 4800-baud link, which must still assemble one frame.
type dribblePort struct {
	data []byte
	step int
	idle int // empty reads before each chunk
	n    int
}

func (p *dribblePort) Read(b []byte) (int, error) {
	if p.n < p.idle {
		p.n++
		return 0, nil
	}
	p.n = 0
	if len(p.data) == 0 {
		return 0, nil
	}
	k := p.step
	if k > len(p.data) {
		k = len(p.data)
	}
	if k > len(b) {
		k = len(b)
	}
	copy(b, p.data[:k])
	p.data = p.data[k:]
	return k, nil
}
func (p *dribblePort) Write(b []byte) (int, error) { return len(b), nil }
func (p *dribblePort) Close() error                { return nil }

func TestReadFrameAssemblesASlowFrame(t *testing.T) {
	want := []byte{'@', 'A', 0x00, 0x20, 0x1E, 0xA8, 0x00, 0x05, 0x30, 0x37, 0x0D}
	p := &dribblePort{data: append([]byte(nil), want...), step: 3, idle: 2}
	buf := make([]byte, 11)
	if err := readFrame(p, buf, time.Second); err != nil {
		t.Fatalf("readFrame: %v", err)
	}
	for i := range want {
		if buf[i] != want[i] {
			t.Fatalf("read % X, want % X", buf, want)
		}
	}
}

// A truncated frame is a failure, not a frame. The controller sending 5 bytes
// and stopping used to spin forever on the missing 6.
func TestReadFrameRejectsATruncatedFrame(t *testing.T) {
	p := &dribblePort{data: []byte{'@', 'A', 0x00, 0x20, 0x1E}, step: 5}
	err := readFrame(p, make([]byte, 11), 100*time.Millisecond)
	if err == nil {
		t.Fatal("a 5-byte frame was accepted as 11")
	}
}

// A real error still comes straight back.
func TestReadFrameReturnsPortErrors(t *testing.T) {
	want := errors.New("port closed")
	p := errPort{err: want}
	if err := readFrame(p, make([]byte, 11), time.Second); !errors.Is(err, want) {
		t.Fatalf("err = %v, want %v", err, want)
	}
}

type errPort struct{ err error }

func (p errPort) Read([]byte) (int, error)    { return 0, p.err }
func (p errPort) Write(b []byte) (int, error) { return len(b), nil }
func (p errPort) Close() error                { return nil }

var _ io.ReadWriteCloser = errPort{}
