package ipc

import (
	"bytes"
	"encoding/binary"
	"errors"
	"io"
	"testing"
)

// The framing is hand-rolled, and it is the protocol all nine daemons depend
// on, so a bug in it is a bug everywhere (Vikunja #410). The review asked
// whether to replace it with something standard. It stays: length-prefixed
// JSON over a unix socket is ninety lines, it is readable with socat, and the
// alternatives (net/rpc, gRPC, a codec library) all buy schema machinery this
// boundary does not want. What was wrong was inheriting it untested. These
// are the paths a real socket produces that the round-trip test never does.

// A short header is not EOF. EOF means the peer closed cleanly between
// frames, which the server treats as a normal disconnect; a header that stops
// halfway is a truncated frame and must be reported as an error, or a peer
// that dies mid-write looks like one that hung up politely.
func TestReadFrameTruncatedHeader(t *testing.T) {
	var v any
	err := readFrame(bytes.NewReader([]byte{0, 0, 4}), &v)
	if err == nil {
		t.Fatal("a three-byte header must fail")
	}
	if errors.Is(err, io.EOF) {
		t.Fatalf("err = %v, want a truncation error, not EOF", err)
	}
}

// A header promising more body than follows. Same reasoning: the frame never
// arrived, so it must not decode into a zero value the caller then trusts.
func TestReadFrameTruncatedBody(t *testing.T) {
	var buf bytes.Buffer
	var hdr [4]byte
	binary.BigEndian.PutUint32(hdr[:], 32)
	buf.Write(hdr[:])
	buf.WriteString(`{"m":"pi`)

	var got map[string]string
	if err := readFrame(&buf, &got); err == nil {
		t.Fatal("a body shorter than its prefix must fail")
	}
	if len(got) != 0 {
		t.Errorf("decoded %v from a truncated frame", got)
	}
}

// Nothing at all is EOF, and only this is.
func TestReadFrameEmptyIsEOF(t *testing.T) {
	var v any
	if err := readFrame(bytes.NewReader(nil), &v); !errors.Is(err, io.EOF) {
		t.Fatalf("err = %v, want io.EOF", err)
	}
}

// byteAtATime returns one byte per Read, which is what a socket is allowed to
// do and what a bytes.Reader never does. readFrame uses io.ReadFull for both
// the header and the body; this is the test that would fail if either turned
// into a bare Read.
type byteAtATime struct {
	b []byte
	i int
}

func (r *byteAtATime) Read(p []byte) (int, error) {
	if r.i >= len(r.b) {
		return 0, io.EOF
	}
	if len(p) == 0 {
		return 0, nil
	}
	p[0] = r.b[r.i]
	r.i++
	return 1, nil
}

func TestReadFrameReassemblesPartialReads(t *testing.T) {
	type payload struct {
		Msg string `json:"m"`
		N   int    `json:"n"`
	}
	want := payload{Msg: "привет", N: 7}
	var buf bytes.Buffer
	if err := writeFrame(&buf, want); err != nil {
		t.Fatalf("writeFrame: %v", err)
	}

	var got payload
	if err := readFrame(&byteAtATime{b: buf.Bytes()}, &got); err != nil {
		t.Fatalf("readFrame: %v", err)
	}
	if got != want {
		t.Fatalf("got %+v, want %+v", got, want)
	}
}

// Two frames written back to back must come back as two frames. A reader that
// consumed more than one frame's body would desynchronize the connection, and
// the symptom would be a reply attributed to the wrong request.
func TestReadFrameStopsAtTheFrameBoundary(t *testing.T) {
	var buf bytes.Buffer
	for _, m := range []string{"first", "second"} {
		if err := writeFrame(&buf, map[string]string{"m": m}); err != nil {
			t.Fatalf("writeFrame: %v", err)
		}
	}
	r := &byteAtATime{b: buf.Bytes()}
	for _, want := range []string{"first", "second"} {
		var got map[string]string
		if err := readFrame(r, &got); err != nil {
			t.Fatalf("readFrame(%s): %v", want, err)
		}
		if got["m"] != want {
			t.Fatalf("got %q, want %q", got["m"], want)
		}
	}
	var extra map[string]string
	if err := readFrame(r, &extra); !errors.Is(err, io.EOF) {
		t.Fatalf("after two frames: err = %v, want io.EOF", err)
	}
}

// A body that is not JSON is an error, not a zero value. The peer is either
// broken or not speaking this protocol; either way the caller must not read
// on as though it decoded.
func TestReadFrameRejectsNonJSONBody(t *testing.T) {
	var buf bytes.Buffer
	var hdr [4]byte
	body := []byte("not json at all")
	binary.BigEndian.PutUint32(hdr[:], uint32(len(body)))
	buf.Write(hdr[:])
	buf.Write(body)

	var got map[string]string
	if err := readFrame(&buf, &got); err == nil {
		t.Fatal("a non-JSON body must fail")
	}
}
