package pkg_test

import (
	"bytes"
	"io"
	"io/fs"
	"maps"
	"reflect"
	"testing"
	"testing/fstest"
	"unsafe"

	"hakurei.app/check"
	"hakurei.app/internal/pkg"
)

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

	type entry struct {
		path string
		mode fs.FileMode
		data string
	}
	testCases := []struct {
		name    string
		fsys    fs.FS
		entries []entry
		sum     pkg.Checksum
		err     error
	}{
		{"bad type", fstest.MapFS{
			".":       {Mode: fs.ModeDir | 0700},
			"invalid": {Mode: fs.ModeCharDevice | 0400},
		}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
			fs.ModeCharDevice | 0400,
		)},

		{"coldboot", fstest.MapFS{
			".": {Mode: fs.ModeDir | 0700},

			"devices":        {Mode: fs.ModeDir | 0700},
			"devices/uevent": {Mode: 0600, Data: []byte("add")},
			"devices/empty":  {Mode: fs.ModeDir | 0700},

			"devices/sub":        {Mode: fs.ModeDir | 0700},
			"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},

			"block":        {Mode: fs.ModeDir | 0700},
			"block/uevent": {Mode: 0600},
		}, []entry{
			{".", fs.ModeDir | 0700, ""},

			{"block", fs.ModeDir | 0700, ""},
			{"block/uevent", 0600, ""},

			{"devices", fs.ModeDir | 0700, ""},
			{"devices/empty", fs.ModeDir | 0700, ""},
			{"devices/sub", fs.ModeDir | 0700, ""},
			{"devices/sub/uevent", 0600, "add"},
			{"devices/uevent", 0600, "add"},
		}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},

		{"empty", fstest.MapFS{
			".":          {Mode: fs.ModeDir | 0700},
			"checksum":   {Mode: fs.ModeDir | 0700},
			"identifier": {Mode: fs.ModeDir | 0700},
			"work":       {Mode: fs.ModeDir | 0700},
		}, []entry{
			{".", fs.ModeDir | 0700, ""},
			{"checksum", fs.ModeDir | 0700, ""},
			{"identifier", fs.ModeDir | 0700, ""},
			{"work", fs.ModeDir | 0700, ""},
		}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C"), nil},

		{"sample directory step garbage", fstest.MapFS{
			".": {Mode: fs.ModeDir | 0500},

			"lib":       {Mode: fs.ModeDir | 0500},
			"lib/check": {Mode: 0400},

			"lib/pkgconfig": {Mode: fs.ModeDir | 0500},
		}, []entry{
			{".", fs.ModeDir | 0500, ""},

			{"lib", fs.ModeDir | 0500, ""},
			{"lib/check", 0400, ""},

			{"lib/pkgconfig", fs.ModeDir | 0500, ""},
		}, pkg.MustDecode("CUx-3hSbTWPsbMfDhgalG4Ni_GmR9TnVX8F99tY_P5GtkYvczg9RrF5zO0jX9XYT"), nil},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

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

				var buf bytes.Buffer
				if err := pkg.Write(
					tc.fsys,
					".",
					&buf,
				); !reflect.DeepEqual(err, tc.err) {
					t.Fatalf("Flatten: error = %v, want %v", err, tc.err)
				} else if tc.err != nil {
					return
				}

				r := pkg.NewReader(bytes.NewReader(buf.Bytes()))
				var got []entry
				for {
					h, err := r.Next()
					if err != nil {
						if err == io.EOF {
							break
						}
						t.Fatalf("Next: error = %v", err)
					}

					var data []byte
					if data, err = io.ReadAll(r); err != nil {
						t.Fatalf("Read: error = %v", err)
					}

					got = append(got, entry{
						path: h.Path,
						mode: h.Mode,
						data: unsafe.String(unsafe.SliceData(data), len(data)),
					})
				}

				if !reflect.DeepEqual(got, tc.entries) {
					t.Fatalf("Reader: %#v, want %#v", got, tc.entries)
				}
			})

			if tc.err != nil {
				return
			}

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

				var got pkg.Checksum
				if err := pkg.SumFS(&got, tc.fsys, "."); err != nil {
					t.Fatalf("SumFS: error = %v", err)
				} else if got != tc.sum {
					t.Fatalf("SumFS: %v", &pkg.ChecksumMismatchError{
						Got:  got,
						Want: tc.sum,
					})
				}
			})
		})
	}
}

var archiveTestdata = fstest.MapFS{
	".": {Mode: fs.ModeDir | 0700},

	"devices":        {Mode: fs.ModeDir | 0700},
	"devices/uevent": {Mode: 0600, Data: []byte("add")},
	"devices/empty":  {Mode: fs.ModeDir | 0700},

	"devices/sub":        {Mode: fs.ModeDir | 0700},
	"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},

	"block":        {Mode: fs.ModeDir | 0700},
	"block/uevent": {Mode: 0600},
}

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

	want := maps.Clone(archiveTestdata)
	want["."].Mode = fs.ModeDir | 0500

	checkWithCache(t, []cacheTestCase{
		{"unpack", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
			var buf bytes.Buffer
			if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
				t.Fatal(err)
			}

			cureMany(t, c, []cureStep{
				{"sample", pkg.NewArchive(
					pkg.NewFile("", buf.Bytes()),
				), ignorePathname, expectsFS(want), pkg.WNew, nil},
			})
		}, expectsFS{
			".": {Mode: fs.ModeDir | 0700},

			"checksum": {Mode: fs.ModeDir | 0700},
			"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F":                    {Mode: fs.ModeDir | 0500},
			"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block":              {Mode: fs.ModeDir | 0700},
			"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block/uevent":       {Mode: 0600},
			"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices":            {Mode: fs.ModeDir | 0700},
			"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/empty":      {Mode: fs.ModeDir | 0700},
			"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub":        {Mode: fs.ModeDir | 0700},
			"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
			"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/uevent":     {Mode: 0600, Data: []byte("add")},

			"identifier": {Mode: fs.ModeDir | 0700},
			"identifier/3oYyAbRJ_we7AgWo1BRcRcnxXFk3mAQ0Qui2nGQMi8GIJNJQtvUC6P2IeoA5mbjD": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F")},

			"substitute": {Mode: fs.ModeDir | 0700},
			"work":       {Mode: fs.ModeDir | 0700},
		}},
	})
}

func BenchmarkArchiveRead(b *testing.B) {
	var buf bytes.Buffer
	if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
		b.Fatal(err)
	}
	testdata := buf.Bytes()

	for b.Loop() {
		r := pkg.NewReader(bytes.NewReader(testdata))
		for {
			_, err := r.Next()
			if err != nil {
				if err == io.EOF {
					break
				}
				b.Fatal(err)
			}
		}
	}
}

func BenchmarkArchiveWrite(b *testing.B) {
	for b.Loop() {
		if err := pkg.Write(archiveTestdata, ".", io.Discard); err != nil {
			b.Fatal(err)
		}
	}
}
