package winpwn

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

// newPipeTube wires a Tube to an in-memory io.Pipe pair instead of a real
// process/socket, so Tube's framing logic (Send/Recv/RecvUntil/timeouts) can
// be unit-tested without spawning anything OS-specific. Returns the tube
// plus the "remote" end the test drives directly.
func newPipeTube() (tube *Tube, remoteRead *io.PipeReader, remoteWrite *io.PipeWriter) {
	toTube, fromRemote := io.Pipe()   // remote writes fromRemote -> tube reads toTube
	toRemote, fromTube := io.Pipe()   // tube writes fromTube -> remote reads toRemote
	tube = newTube(nil, nil, fromTube, toTube)
	return tube, toRemote, fromRemote
}

func TestTubeSendRecv(t *testing.T) {
	tube, remoteRead, remoteWrite := newPipeTube()
	defer tube.Close()

	go func() {
		buf := make([]byte, 5)
		_, _ = io.ReadFull(remoteRead, buf)
		_, _ = remoteWrite.Write([]byte("got: "))
		_, _ = remoteWrite.Write(buf)
	}()

	if err := tube.Send([]byte("hello")); err != nil {
		t.Fatal(err)
	}

	got, err := tube.Recv(10)
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != "got: hello" {
		t.Errorf("Recv = %q, want %q", got, "got: hello")
	}
}

func TestTubeRecvUntil(t *testing.T) {
	tube, _, remoteWrite := newPipeTube()
	defer tube.Close()

	go func() {
		_, _ = remoteWrite.Write([]byte("foo bar: baz\n"))
	}()

	got, err := tube.RecvUntil([]byte(": "))
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != "foo bar: " {
		t.Errorf("RecvUntil = %q, want %q", got, "foo bar: ")
	}
}

func TestTubeRecvLineUsesContextNewline(t *testing.T) {
	tube, _, remoteWrite := newPipeTube()
	defer tube.Close()

	go func() {
		_, _ = remoteWrite.Write([]byte("first line\nsecond"))
	}()

	got, err := tube.RecvLine()
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != "first line\n" {
		t.Errorf("RecvLine = %q, want %q", got, "first line\n")
	}
}

func TestTubeSendAfter(t *testing.T) {
	tube, remoteRead, remoteWrite := newPipeTube()
	defer tube.Close()

	go func() {
		_, _ = remoteWrite.Write([]byte("password: "))
		buf := make([]byte, 6)
		_, _ = io.ReadFull(remoteRead, buf)
		_, _ = remoteWrite.Write(buf)
	}()

	if err := tube.SendAfter([]byte(": "), []byte("secret")); err != nil {
		t.Fatal(err)
	}
	got, err := tube.Recv(6)
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != "secret" {
		t.Errorf("echoed payload = %q, want %q", got, "secret")
	}
}

func TestTubeRecvTimesOutOnHang(t *testing.T) {
	tube, _, _ := newPipeTube() // nobody ever writes
	defer tube.Close()
	tube.SetTimeout(50 * time.Millisecond)

	start := time.Now()
	_, err := tube.Recv(1)
	if err == nil {
		t.Fatal("expected a timeout error, got nil")
	}
	if elapsed := time.Since(start); elapsed > time.Second {
		t.Errorf("timeout took %s, expected ~50ms", elapsed)
	}
}

func TestTubeCloseIsIdempotent(t *testing.T) {
	tube, _, _ := newPipeTube()
	if err := tube.Close(); err != nil {
		t.Fatalf("first Close: %v", err)
	}
	if err := tube.Close(); err != nil {
		t.Fatalf("second Close should be a no-op, got: %v", err)
	}
}
