package message_test

import (
	"bytes"
	"errors"
	"reflect"
	"strconv"
	"syscall"
	"testing"

	"hakurei.app/internal/stub"
	"hakurei.app/message"
)

func TestSuspendable(t *testing.T) {
	// copied from output.go
	const suspendBufMax = 1 << 24
	t.Parallel()

	const (
		// equivalent to len(want.pt)
		nSpecialPtEquiv = -iota - 1
		// equivalent to len(want.w)
		nSpecialWEquiv
		// suspends writer before executing test case, implies nSpecialWEquiv
		nSpecialSuspend
		// offset: resume writer and measure against dump instead, implies nSpecialPtEquiv
		nSpecialDump
	)

	// shares the same writer
	steps := []struct {
		name    string
		w, pt   []byte
		err     error
		wantErr error
		n       int
	}{
		{"simple", []byte{0xde, 0xad, 0xbe, 0xef}, []byte{0xde, 0xad, 0xbe, 0xef},
			nil, nil, nSpecialPtEquiv},

		{"error", []byte{0xb, 0xad}, []byte{0xb, 0xad},
			stub.UniqueError(0), stub.UniqueError(0), nSpecialPtEquiv},

		{"suspend short", []byte{0}, nil,
			nil, nil, nSpecialSuspend},
		{"sw short 0", []byte{0xca, 0xfe, 0xba, 0xbe}, nil,
			nil, nil, nSpecialWEquiv},
		{"sw short 1", []byte{0xff}, nil,
			nil, nil, nSpecialWEquiv},
		{"resume short", nil, []byte{0, 0xca, 0xfe, 0xba, 0xbe, 0xff}, nil, nil,
			nSpecialDump},

		{"long pt", bytes.Repeat([]byte{0xff}, suspendBufMax+1), bytes.Repeat([]byte{0xff}, suspendBufMax+1),
			nil, nil, nSpecialPtEquiv},

		{"suspend fill", bytes.Repeat([]byte{0xfe}, suspendBufMax), nil,
			nil, nil, nSpecialSuspend},
		{"drop", []byte{0}, nil,
			nil, syscall.ENOMEM, 0},
		{"drop error", []byte{0}, nil,
			stub.UniqueError(1), syscall.ENOMEM, 0},
		{"resume fill", nil, bytes.Repeat([]byte{0xfe}, suspendBufMax),
			nil, nil, nSpecialDump - 2},

		{"suspend fill partial", bytes.Repeat([]byte{0xfd}, suspendBufMax-0xf), nil,
			nil, nil, nSpecialSuspend},
		{"partial write", bytes.Repeat([]byte{0xad}, 0x1f), nil,
			nil, syscall.ENOMEM, 0xf},
		{"full drop", []byte{0}, nil,
			nil, syscall.ENOMEM, 0},
		{"resume fill partial", nil, append(bytes.Repeat([]byte{0xfd}, suspendBufMax-0xf), bytes.Repeat([]byte{0xad}, 0xf)...),
			nil, nil, nSpecialDump - 0x10 - 1},
	}

	var dw expectWriter

	w := message.Suspendable{Downstream: &dw}
	for _, step := range steps {
		// these share the same writer, so cannot be subtests
		t.Logf("writing step %q", step.name)
		dw.expect, dw.err = step.pt, step.err

		var (
			gotN   int
			gotErr error
		)

		wantN := step.n
		switch wantN {
		case nSpecialPtEquiv:
			wantN = len(step.pt)
			gotN, gotErr = w.Write(step.w)

		case nSpecialWEquiv:
			wantN = len(step.w)
			gotN, gotErr = w.Write(step.w)

		case nSpecialSuspend:
			s := w.IsSuspended()
			if ok := w.Suspend(); s && ok {
				t.Fatal("Suspend: unexpected success")
			}

			wantN = len(step.w)
			gotN, gotErr = w.Write(step.w)

		default:
			if wantN <= nSpecialDump {
				if !w.IsSuspended() {
					t.Fatal("IsSuspended unexpected false")
				}

				resumed, dropped, n, err := w.Resume()
				if !resumed {
					t.Fatal("Resume: resumed = false")
				}
				if wantDropped := nSpecialDump - wantN; int(dropped) != wantDropped {
					t.Errorf("Resume: dropped = %d, want %d", dropped, wantDropped)
				}

				wantN = len(step.pt)
				gotN, gotErr = int(n), err
			} else {
				gotN, gotErr = w.Write(step.w)
			}
		}

		if gotN != wantN {
			t.Errorf("Write: n = %d, want %d", gotN, wantN)
		}

		if !reflect.DeepEqual(gotErr, step.wantErr) {
			t.Errorf("Write: %v", gotErr)
		}

		if dw.expect != nil {
			t.Errorf("expect: %q", string(dw.expect))
		}
	}
}

// expectWriter compares Write calls to expect.
type expectWriter struct {
	expect []byte
	err    error

	// optional consecutive write
	next []byte

	// optional, calls Error on failure if not nil
	t *testing.T
}

func (w *expectWriter) Write(p []byte) (n int, err error) {
	defer func() { w.expect = w.next; w.next = nil }()

	n, err = len(p), w.err
	if w.expect == nil {
		n, err = 0, errors.New("unexpected call to Write: "+strconv.Quote(string(p)))
		if w.t != nil {
			w.t.Error(err.Error())
		}
		return
	}
	if string(p) != string(w.expect) {
		n, err = 0, errors.New("p = "+strconv.Quote(string(p))+", want "+strconv.Quote(string(w.expect)))
		if w.t != nil {
			w.t.Error(err.Error())
		}
		return
	}
	return
}
