package archive

import (
	"archive/tar"
	"archive/zip"
	"bytes"
	"compress/gzip"
	"io"
	"testing"

	"github.com/ulikunitz/xz"
)

// buildTar writes a small dir+file tree as a tar stream.
func buildTar(t *testing.T, w io.Writer) {
	t.Helper()
	tw := tar.NewWriter(w)
	if err := tw.WriteHeader(&tar.Header{Name: "factorio/", Typeflag: tar.TypeDir, Mode: 0o755}); err != nil {
		t.Fatal(err)
	}
	body := []byte("#!/bin/sh\necho hi\n")
	if err := tw.WriteHeader(&tar.Header{Name: "factorio/bin/x64/factorio", Typeflag: tar.TypeReg, Mode: 0o755, Size: int64(len(body))}); err != nil {
		t.Fatal(err)
	}
	if _, err := tw.Write(body); err != nil {
		t.Fatal(err)
	}
	if err := tw.Close(); err != nil {
		t.Fatal(err)
	}
}

func makeTarXz(t *testing.T) []byte {
	t.Helper()
	var buf bytes.Buffer
	xw, err := xz.NewWriter(&buf)
	if err != nil {
		t.Fatal(err)
	}
	buildTar(t, xw)
	if err := xw.Close(); err != nil {
		t.Fatal(err)
	}
	return buf.Bytes()
}

func makeTarGz(t *testing.T) []byte {
	t.Helper()
	var buf bytes.Buffer
	gw := gzip.NewWriter(&buf)
	buildTar(t, gw)
	if err := gw.Close(); err != nil {
		t.Fatal(err)
	}
	return buf.Bytes()
}

func makeZip(t *testing.T) []byte {
	t.Helper()
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	f, err := zw.Create("factorio/bin/x64/factorio")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := f.Write([]byte("#!/bin/sh\necho hi\n")); err != nil {
		t.Fatal(err)
	}
	if err := zw.Close(); err != nil {
		t.Fatal(err)
	}
	return buf.Bytes()
}

func TestDetectFormatMagicBytes(t *testing.T) {
	// Extensionless names — magic bytes must carry detection (factorio's
	// download URL has no file extension).
	cases := []struct {
		data []byte
		want string
	}{
		{makeTarXz(t), "tar.xz"},
		{makeTarGz(t), "tar.gz"},
		{makeZip(t), "zip"},
		{[]byte("plain text, not an archive"), ""},
	}
	for _, c := range cases {
		if got := DetectFormat(c.data, "https://example.com/get-download/stable/headless/linux64"); got != c.want {
			t.Errorf("DetectFormat = %q, want %q", got, c.want)
		}
	}
}

func TestDetectFormatExtensionFallback(t *testing.T) {
	// Content too short for magic — extension hint decides.
	if got := DetectFormat([]byte{}, "thing.tar.xz"); got != "tar.xz" {
		t.Errorf("extension fallback = %q, want tar.xz", got)
	}
	if got := DetectFormat([]byte{}, "thing.tgz"); got != "tar.gz" {
		t.Errorf("extension fallback = %q, want tar.gz", got)
	}
}

func TestWalkAllFormats(t *testing.T) {
	for name, data := range map[string][]byte{
		"tar.xz": makeTarXz(t),
		"tar.gz": makeTarGz(t),
		"zip":    makeZip(t),
	} {
		var files []string
		var content []byte
		entries, _, err := Walk(data, "blob", func(n string, mode int64, isDir bool, body io.Reader) error {
			if isDir {
				return nil
			}
			files = append(files, n)
			b, err := io.ReadAll(body)
			content = b
			return err
		})
		if err != nil {
			t.Fatalf("%s: Walk: %v", name, err)
		}
		if entries == 0 || len(files) != 1 || files[0] != "factorio/bin/x64/factorio" {
			t.Fatalf("%s: unexpected entries %d files %v", name, entries, files)
		}
		if string(content) != "#!/bin/sh\necho hi\n" {
			t.Fatalf("%s: content mismatch: %q", name, content)
		}
	}
}

func TestWriteAsTarRoundTrip(t *testing.T) {
	var out bytes.Buffer
	entries, bytesOut, err := WriteAsTar(makeTarXz(t), "blob", &out)
	if err != nil {
		t.Fatal(err)
	}
	if entries != 2 {
		t.Fatalf("entries = %d, want 2", entries)
	}
	if bytesOut == 0 {
		t.Fatal("bytesOut = 0")
	}
	// Re-walk the emitted tar and check the file mode survived.
	tr := tar.NewReader(&out)
	var sawFile bool
	for {
		hdr, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			t.Fatal(err)
		}
		if hdr.Typeflag == tar.TypeReg {
			sawFile = true
			if hdr.Name != "factorio/bin/x64/factorio" {
				t.Fatalf("name = %q", hdr.Name)
			}
			if hdr.Mode&0o111 == 0 {
				t.Fatalf("exec bit lost: mode %o", hdr.Mode)
			}
		}
	}
	if !sawFile {
		t.Fatal("no regular file in re-emitted tar")
	}
}

func TestSafeEntryPathRejectsTraversal(t *testing.T) {
	for _, bad := range []string{"../evil", "a/../../evil", "..", ""} {
		if got := SafeEntryPath(bad); got != "" {
			t.Errorf("SafeEntryPath(%q) = %q, want rejection", bad, got)
		}
	}
	if got := SafeEntryPath("/abs/path"); got != "abs/path" {
		t.Errorf("leading slash strip = %q", got)
	}
}
