package store

import (
	"bytes"
	"errors"
	"io"
	"io/fs"
	"os"
	"reflect"
	"syscall"
	"testing"
	"time"

	"hakurei.app/hst"
)

func TestEntryHeader(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name string
		data [entryHeaderSize]byte
		et   hst.Enablements
		err  error
	}{
		{"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
			0x0a, 0xf6}, 0,
			errors.New("header enablement value is inconsistent")},
		{"unexpected revision", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0xff, 0xff}, 0,
			errors.New("unexpected revision ffff")},
		{"invalid header", [entryHeaderSize]byte{0x00, 0xfe, 0xca, 0xfe}, 0,
			errors.New("invalid header 00fecafe")},

		{"success high", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
			0xff, 0x00}, 0xff, nil},
		{"success", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
			0x09, 0xf6}, hst.EWayland | hst.EPipeWire, nil},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			t.Run("encode", func(t *testing.T) {
				if tc.err != nil {
					return
				}
				t.Parallel()

				if got := entryHeaderEncode(tc.et); *got != tc.data {
					t.Errorf("entryHeaderEncode: %x, want %x", *got, tc.data)
				}

				t.Run("write", func(t *testing.T) {
					var buf bytes.Buffer
					if err := entryWriteHeader(&buf, tc.et); err != nil {
						t.Fatalf("entryWriteHeader: error = %v", err)
					}
					if got := ([entryHeaderSize]byte)(buf.Bytes()); got != tc.data {
						t.Errorf("entryWriteHeader: %x, want %x", got, tc.data)
					}
				})
			})

			t.Run("decode", func(t *testing.T) {
				t.Parallel()

				got, err := entryHeaderDecode(&tc.data)
				if !reflect.DeepEqual(err, tc.err) {
					t.Fatalf("entryHeaderDecode: error = %#v, want %#v", err, tc.err)
				}
				if err != nil {
					return
				}
				if got != tc.et {
					t.Errorf("entryHeaderDecode: et = %q, want %q", got, tc.et)
				}

				if got, err = entryReadHeader(bytes.NewReader(tc.data[:])); err != nil {
					t.Fatalf("entryReadHeader: error = %#v", err)
				} else if got != tc.et {
					t.Errorf("entryReadHeader: et = %q, want %q", got, tc.et)
				}
			})
		})
	}
}

func TestEntrySizeError(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name string
		err  error
		want string
	}{
		{"size only", &EntrySizeError{Size: 0xdeadbeef},
			`state entry file is too short`},
		{"full", &EntrySizeError{Name: "nonexistent", Size: 0xdeadbeef},
			`state entry file "nonexistent" is too short`},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			if got := tc.err.Error(); got != tc.want {
				t.Errorf("Error: %s, want %s", got, tc.want)
			}
		})
	}
}

func TestEntryCheckFile(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name string
		fi   os.FileInfo
		err  error
	}{
		{"dir", &stubFi{name: "dir", isDir: true},
			syscall.EISDIR},
		{"short", stubFi{name: "short", size: 8},
			&EntrySizeError{Name: "short", Size: 8}},
		{"success", stubFi{size: 9}, nil},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			if err := entryCheckFile(tc.fi); !reflect.DeepEqual(err, tc.err) {
				t.Errorf("entryCheckFile: error = %#v, want %#v", err, tc.err)
			}
		})
	}
}

func TestEntryReadHeader(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name string
		newR func() io.Reader
		err  error
	}{
		{"eof", func() io.Reader { return bytes.NewReader([]byte{}) }, io.EOF},
		{"short", func() io.Reader { return bytes.NewReader([]byte{0}) }, &EntrySizeError{Size: 1}},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			if _, err := entryReadHeader(tc.newR()); !reflect.DeepEqual(err, tc.err) {
				t.Errorf("entryReadHeader: error = %#v, want %#v", err, tc.err)
			}
		})
	}
}

// stubFi partially implements [os.FileInfo] using hardcoded values.
type stubFi struct {
	name  string
	size  int64
	isDir bool
}

func (fi stubFi) Name() string {
	if fi.name == "" {
		panic("unreachable")
	}
	return fi.name
}

func (fi stubFi) Size() int64 {
	if fi.size < 0 {
		panic("unreachable")
	}
	return fi.size
}

func (fi stubFi) IsDir() bool { return fi.isDir }

func (fi stubFi) Mode() fs.FileMode  { panic("unreachable") }
func (fi stubFi) ModTime() time.Time { panic("unreachable") }
func (fi stubFi) Sys() any           { panic("unreachable") }
