package monstersirenfetch_test

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"reflect"
	"strconv"
	"testing"
	"unsafe"

	. "git.gensokyo.uk/yonah/monstersirenfetch"
)

func TestInconsistentEnrichError(t *testing.T) {
	const want = "field cid inconsistent: 48794 differs from 3735928559"
	if got := (&InconsistentEnrichError[StringInt]{Field: "cid", Value: 48794, NewValue: 0xdeadbeef}).Error(); got != want {
		t.Errorf("Error: %q, want %q", got, want)
	}
}

func TestNullableString(t *testing.T) {
	checkJSONRoundTripM(t, []jsonRoundTripTestCase[NullableString]{
		{"zero", nil, `null`, ""},
		{"string", nil, `"null"`, "null"},

		{"invalid json", newSyntaxError("unexpected end of JSON input", 5), `"null`, "\x00"},
	})
}

func TestStringInt(t *testing.T) {
	checkJSONRoundTripM(t, []jsonRoundTripTestCase[StringInt]{
		{"valid long", nil, `"3735928559"`, 0xdeadbeef},
		{"valid pad 4", nil, `"0000"`, 0},
		{"valid 4", nil, `"1000"`, 1e3},
		{"valid pad 6", nil, `"010000"`, 1e4},
		{"valid 6", nil, `"100000"`, 1e5},

		{"invalid json", newSyntaxError("unexpected end of JSON input", 11), `"3735928559`, -1},
		{"invalid number", &strconv.NumError{Func: "Atoi", Num: ":3735928559", Err: strconv.ErrSyntax}, `":3735928559"`, -1},
	})
}

func newSyntaxError(msg string, offset int64) *json.SyntaxError {
	e := &json.SyntaxError{Offset: offset}
	msgV := reflect.ValueOf(e).Elem().FieldByName("msg")
	reflect.NewAt(msgV.Type(), unsafe.Pointer(msgV.UnsafeAddr())).Elem().SetString(msg)
	return e
}

type jsonRoundTripTestCase[T comparable] struct {
	name    string
	wantErr error
	data    string
	val     T
}

func checkJSONRoundTripM[T comparable](t *testing.T, testCases []jsonRoundTripTestCase[T]) {
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			if tc.wantErr == nil {
				t.Run("marshal", func(t *testing.T) {
					if got, err := json.Marshal(&tc.val); err != nil {
						t.Fatalf("Marshal: error = %v", err)
					} else if string(got) != tc.data {
						t.Errorf("Marshal: %s, want %s", string(got), tc.data)
					}
				})
			}

			t.Run("unmarshal", func(t *testing.T) {
				var got T
				err := json.Unmarshal([]byte(tc.data), &got)

				if !reflect.DeepEqual(err, tc.wantErr) {
					t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
				}

				if tc.wantErr == nil {
					if got != tc.val {
						t.Errorf("Unmarshal: %v, want %v", got, tc.val)
					}
				}
			})
		})
	}
}

func checkJSONRoundTrip[T any](t *testing.T, v T, data []byte) {
	t.Run("marshal", func(t *testing.T) {
		buf := new(bytes.Buffer)
		buf.Grow(len(data))
		err := NewEncoder(buf).Encode(&v)
		if err != nil {
			t.Fatalf("Marshal: error = %v", err)
		}
		got := buf.Bytes()[:buf.Len()-1]
		if string(got) != string(data) {
			t.Errorf("Marshal:\n%s\nwant\n%s", string(got), string(data))
		}
	})

	t.Run("unmarshal", func(t *testing.T) {
		var got T
		if err := json.Unmarshal(data, &got); err != nil {
			t.Fatalf("Unmarshal: error = %v", err)
		}
		if !reflect.DeepEqual(&got, &v) {
			t.Errorf("Unmarshal:\n%#v\nwant\n%#v", got, v)
		}
	})
}

type stubNetErrorCloser stubNet

func (n stubNetErrorCloser) Get(ctx context.Context, url string) (io.ReadCloser, int64, error) {
	r, l, err := stubNet(n).Get(ctx, url)
	if r != nil {
		r = errorCloser{r}
	}
	return r, l, err
}

type stubNet map[string][]byte

func (n stubNet) Get(_ context.Context, url string) (io.ReadCloser, int64, error) {
	if b, ok := n[url]; !ok {
		return nil, -2, fmt.Errorf("url %s requested, but is not present", url)
	} else {
		return nopCloser{bytes.NewReader(b)}, int64(len(b)), nil
	}
}

type errorCloser struct{ io.Reader }

func (errorCloser) Close() error { return os.ErrInvalid }

type nopCloser struct{ io.Reader }

func (nopCloser) Close() error { return nil }
