package pkg_test

import (
	"bytes"
	"crypto/sha512"
	"io/fs"
	"log"
	"os"
	"slices"
	"strings"
	"testing"
	"unique"

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

// formatHandles returns a user-facing string representing h.
func formatHandles[T pkg.ID | pkg.Checksum](handles ...unique.Handle[T]) string {
	var buf strings.Builder
	for _, h := range handles {
		buf.WriteString(pkg.Encode(pkg.Checksum(h.Value())))
		buf.WriteString(", ")
	}
	return strings.TrimSuffix(buf.String(), ", ")
}

func TestClean(t *testing.T) {
	t.Parallel()
	ic := pkg.NewIR()

	testCases := []struct {
		name   string
		a      []pkg.Artifact
		keep   []pkg.Artifact
		inputs bool
		want   expectsFS

		wantIdents    []unique.Handle[pkg.ID]
		wantChecksums []unique.Handle[pkg.Checksum]
	}{
		{"simple", []pkg.Artifact{
			pkg.NewFile("file", nil),
		}, nil, false, expectsFS{
			".": {Mode: fs.ModeDir | 0700},

			"checksum": {Mode: fs.ModeDir | 0700},
			"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400},

			"identifier": {Mode: fs.ModeDir | 0700},
			"identifier/h6qx7m0vsyhO6oLEJgY2rKa1c6RZuIZ4QXxZSh__NZJ9VU0MIj5bFB-KuaPcsfpE": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},

			"lock":       {Mode: 0644},
			"variant":    {Mode: 0400},
			"status":     {Mode: fs.ModeDir | 0700},
			"substitute": {Mode: fs.ModeDir | 0700},
			"fault":      {Mode: fs.ModeDir | 0700},
			"work":       {Mode: fs.ModeDir | 0700},
		}, nil, nil},

		{"keep", []pkg.Artifact{
			pkg.NewFile("removed-file", []byte("removed file")),
		}, []pkg.Artifact{
			pkg.NewFile("file", []byte("\xfd")),
		}, false, expectsFS{
			".": {Mode: fs.ModeDir | 0700},

			"checksum": {Mode: fs.ModeDir | 0700},
			"checksum/KgZ-FjbGuU-XP2QEHInpgv-2Zn0cTH5NqFMgTU0XrSdKmSwyC-3baVs1BMCP5spk": {Mode: 0400, Data: []byte("\xfd")},

			"identifier": {Mode: fs.ModeDir | 0700},
			"identifier/tkrOYhQfTRaArk4nNYNjTmHQUmuJMVIoDeAKIQBm3I4uuMimBKXKMIY2gGZqwQg4": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/KgZ-FjbGuU-XP2QEHInpgv-2Zn0cTH5NqFMgTU0XrSdKmSwyC-3baVs1BMCP5spk")},

			"lock":       {Mode: 0644},
			"variant":    {Mode: 0400},
			"status":     {Mode: fs.ModeDir | 0700},
			"substitute": {Mode: fs.ModeDir | 0700},
			"fault":      {Mode: fs.ModeDir | 0700},
			"work":       {Mode: fs.ModeDir | 0700},
		}, []unique.Handle[pkg.ID]{
			ic.Ident(pkg.NewFile("removed-file", []byte("removed file"))),
		}, []unique.Handle[pkg.Checksum]{
			unique.Make(sha512.Sum384([]byte("removed file"))),
		}},

		{"inputs anchored substitute", []pkg.Artifact{
			&stubArtifactF{
				kind:   pkg.KindExec,
				params: []byte("destroyed"),
				deps: []pkg.Artifact{
					pkg.NewFile("destroyed-input", []byte("destroyed")),
				},
				cure: func(f *pkg.FContext) error {
					p := f.GetWorkDir()
					if err := os.MkdirAll(p.String(), 0755); err != nil {
						return err
					}
					return os.WriteFile(p.Append("result").String(), nil, 0444)
				},
			},
		}, []pkg.Artifact{
			&stubArtifactF{
				kind:   pkg.KindExec,
				params: []byte("kept"),
				deps: []pkg.Artifact{
					pkg.NewFile("kept-input", []byte("kept")),
				},
				cure: func(f *pkg.FContext) error {
					p := f.GetWorkDir()
					if err := os.MkdirAll(p.String(), 0755); err != nil {
						return err
					}
					return os.WriteFile(p.Append("result").String(), nil, 0444)
				},
			},
		}, true, expectsFS{
			".": {Mode: fs.ModeDir | 0700},

			"checksum": {Mode: fs.ModeDir | 0700},
			"checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv":        {Mode: 0400, Data: []byte("kept")},
			"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE":        {Mode: fs.ModeDir | 0500},
			"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE/result": {Mode: 0444},

			"identifier": {Mode: fs.ModeDir | 0700},
			"identifier/KpxTUsUfGbsX6C6HU2j9cd9He3_tt2o2m2MgH4m0zial8lmoqSWY-Gc3KrS0k-UK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv")},
			"identifier/xxQ43VMIS1WHMSEFnUdBNd6pulbtgiTorP_mtJpEsZENYhGBiDEL6Y2D8yqn0Sra": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},

			"lock":    {Mode: 0644},
			"variant": {Mode: 0400},
			"status":  {Mode: fs.ModeDir | 0700},

			"substitute": {Mode: fs.ModeDir | 0700},
			"substitute/dP-6_wIDRRouOaOF-nkBy-IaUbLdYHbOUTrptu7j_qfW01mnHSjYJ0oykUmvUd2x": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
			"substitute/td1tf1kb3z8iUFg2k1xlHeTTEz3xR4e77WSWdv2JhjmZyY6iuvodhdT8BblDOi0E": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},

			"fault": {Mode: fs.ModeDir | 0700},
			"work":  {Mode: fs.ModeDir | 0700},
		}, []unique.Handle[pkg.ID]{
			ic.Ident(pkg.NewFile("destroyed-input", []byte("destroyed"))),
			ic.Ident(&stubArtifactF{
				kind:   pkg.KindExec,
				params: []byte("destroyed"),
				deps: []pkg.Artifact{
					pkg.NewFile("destroyed-input", []byte("destroyed")),
				},
			}),
		}, []unique.Handle[pkg.Checksum]{
			unique.Make(sha512.Sum384([]byte("destroyed"))),
		}},

		{"inputs", []pkg.Artifact{
			&stubArtifactF{
				kind:   pkg.KindExec,
				params: []byte("destroyed"),
				deps: []pkg.Artifact{
					pkg.NewFile("destroyed-input", []byte("destroyed")),
				},
				cure: func(f *pkg.FContext) error {
					if w, err := f.GetStatusWriter(); err != nil {
						return err
					} else if _, err = w.Write([]byte("destroyed")); err != nil {
						return err
					}

					p := f.GetWorkDir()
					if err := os.MkdirAll(p.String(), 0755); err != nil {
						return err
					}
					return os.WriteFile(p.Append("result").String(), nil, 0444)
				},
			},
		}, []pkg.Artifact{
			&stubArtifactF{
				kind:   pkg.KindExec,
				params: []byte("kept"),
				deps: []pkg.Artifact{
					pkg.NewFile("kept-input", []byte("kept")),
				},
				cure: func(f *pkg.FContext) error {
					if w, err := f.GetStatusWriter(); err != nil {
						return err
					} else if _, err = w.Write([]byte("kept")); err != nil {
						return err
					}

					p := f.GetWorkDir()
					if err := os.MkdirAll(p.String(), 0755); err != nil {
						return err
					}
					return os.WriteFile(p.Append("result").String(), []byte{0}, 0444)
				},
			},
		}, true, expectsFS{
			".": {Mode: fs.ModeDir | 0700},

			"checksum": {Mode: fs.ModeDir | 0700},
			"checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W":        {Mode: fs.ModeDir | 0500},
			"checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W/result": {Mode: 0444, Data: []byte("\x00")},
			"checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv":        {Mode: 0400, Data: []byte("kept")},

			"identifier": {Mode: fs.ModeDir | 0700},
			"identifier/KpxTUsUfGbsX6C6HU2j9cd9He3_tt2o2m2MgH4m0zial8lmoqSWY-Gc3KrS0k-UK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv")},
			"identifier/xxQ43VMIS1WHMSEFnUdBNd6pulbtgiTorP_mtJpEsZENYhGBiDEL6Y2D8yqn0Sra": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W")},

			"lock":    {Mode: 0644},
			"variant": {Mode: 0400},

			"status": {Mode: fs.ModeDir | 0700},
			"status/xxQ43VMIS1WHMSEFnUdBNd6pulbtgiTorP_mtJpEsZENYhGBiDEL6Y2D8yqn0Sra": {Mode: 0400, Data: []byte(statusHeader + "kept")},
			"status/td1tf1kb3z8iUFg2k1xlHeTTEz3xR4e77WSWdv2JhjmZyY6iuvodhdT8BblDOi0E": {Mode: 0400, Data: []byte(statusHeader + "kept")},

			"substitute": {Mode: fs.ModeDir | 0700},
			"substitute/td1tf1kb3z8iUFg2k1xlHeTTEz3xR4e77WSWdv2JhjmZyY6iuvodhdT8BblDOi0E": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W")},

			"fault": {Mode: fs.ModeDir | 0700},
			"work":  {Mode: fs.ModeDir | 0700},
		}, []unique.Handle[pkg.ID]{
			ic.Ident(pkg.NewFile("destroyed-input", []byte("destroyed"))),
			ic.Ident(&stubArtifactF{
				kind:   pkg.KindExec,
				params: []byte("destroyed"),
				deps: []pkg.Artifact{
					pkg.NewFile("destroyed-input", []byte("destroyed")),
				},
			}),
		}, []unique.Handle[pkg.Checksum]{
			unique.Make(expectsFS{
				".":      {Mode: fs.ModeDir | 0500},
				"result": {Mode: 0444},
			}.hash()),
			unique.Make(sha512.Sum384([]byte("destroyed"))),
		}},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			base := makeBase(t)
			msg := message.New(log.New(os.Stderr, "clean: ", 0))
			msg.SwapVerbose(testing.Verbose())
			c, err := pkg.Open(t.Context(), msg, base, nil)
			if err != nil {
				t.Fatal(err)
			}
			t.Cleanup(c.Close)

			all := pkg.Collect(slices.Concat(tc.a, tc.keep))
			if _, _, err = c.Cure(&all); !pkg.IsCollected(err) {
				t.Fatal(err)
			}

			var (
				idents    []unique.Handle[pkg.ID]
				checksums []unique.Handle[pkg.Checksum]
			)
			idents, checksums, err = c.Clean(false, tc.inputs, tc.keep...)
			if err != nil {
				t.Fatalf("Clean: error = %v", err)
			}
			var buf [2]pkg.Checksum

			slices.SortFunc(idents, func(a, b unique.Handle[pkg.ID]) int {
				buf[0], buf[1] = a.Value(), b.Value()
				return bytes.Compare(buf[0][:], buf[1][:])
			})
			slices.SortFunc(checksums, func(a, b unique.Handle[pkg.Checksum]) int {
				buf[0], buf[1] = a.Value(), b.Value()
				return bytes.Compare(buf[0][:], buf[1][:])
			})

			if !slices.Equal(idents, tc.wantIdents) {
				t.Errorf(
					"Clean: idents = %s, want %s",
					formatHandles(idents...), formatHandles(tc.wantIdents...),
				)
			}
			if !slices.Equal(checksums, tc.wantChecksums) {
				t.Errorf(
					"Clean: checksums = %s, want %s",
					formatHandles(checksums...), formatHandles(tc.wantChecksums...),
				)
			}

			want := tc.want.hash()
			var checksum pkg.Checksum
			if err = pkg.SumDir(&checksum, base); err != nil {
				t.Fatalf("SumDir: error = %v", err)
			} else if checksum != want {
				t.Error(expectsFrom(base.String()))
			}
		})
	}
}
