package updater

import (
	"archive/tar"
	"bytes"
	"context"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/ulikunitz/xz"

	modulepkg "github.com/dbledeez/panel/pkg/module"
)

// makeFactorioTarXz builds a tiny tar.xz shaped like factorio's
// headless tarball (factorio/bin/x64/factorio).
func makeFactorioTarXz(t *testing.T) []byte {
	t.Helper()
	var buf bytes.Buffer
	xw, err := xz.NewWriter(&buf)
	if err != nil {
		t.Fatal(err)
	}
	tw := tar.NewWriter(xw)
	body := []byte("ELF-not-really")
	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)
	}
	if err := xw.Close(); err != nil {
		t.Fatal(err)
	}
	return buf.Bytes()
}

func testCtx(t *testing.T, dataPath string) *Context {
	t.Helper()
	return &Context{
		InstanceID: "test",
		DataPath:   dataPath,
		Log:        func(string) {},
	}
}

// The factorio case: archive content + extensionless URL + extract:true
// must land the extracted tree under TargetPath, not a raw blob.
func TestDirectExtractsArchiveIntoTargetDir(t *testing.T) {
	blob := makeFactorioTarXz(t)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write(blob)
	}))
	defer srv.Close()

	dir := t.TempDir()
	spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/get-download/stable/headless/linux64", TargetPath: "/game", Extract: true}
	if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err != nil {
		t.Fatal(err)
	}
	bin := filepath.Join(dir, "game", "factorio", "bin", "x64", "factorio")
	data, err := os.ReadFile(bin)
	if err != nil {
		t.Fatalf("expected extracted binary at %s: %v", bin, err)
	}
	if string(data) != "ELF-not-really" {
		t.Fatalf("binary content mismatch: %q", data)
	}
	// The old bug: raw blob written AS the target path.
	if fi, err := os.Stat(filepath.Join(dir, "game")); err != nil || !fi.IsDir() {
		t.Fatalf("target path should be a directory, got err=%v", err)
	}
}

// Same archive, no explicit extract flag: extensionless TargetPath
// still triggers extraction (directory heuristic).
func TestDirectHeuristicExtractsWithoutFlag(t *testing.T) {
	blob := makeFactorioTarXz(t)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write(blob)
	}))
	defer srv.Close()

	dir := t.TempDir()
	spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/linux64", TargetPath: "/game"}
	if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(dir, "game", "factorio", "bin", "x64", "factorio")); err != nil {
		t.Fatalf("expected extraction: %v", err)
	}
}

// The terraria/minecraft guard: a TargetPath WITH a file extension is
// written verbatim even when the content has archive magic (jars are
// zips; terraria's entrypoint unzips its own .zip).
func TestDirectWritesArchiveVerbatimWhenTargetHasExtension(t *testing.T) {
	// A zip blob (like a .jar or terraria-server zip).
	var zipBuf bytes.Buffer
	zipBuf.Write([]byte{0x50, 0x4b, 0x03, 0x04}) // zip local-header magic
	zipBuf.WriteString("rest-of-zip-not-parsed")

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write(zipBuf.Bytes())
	}))
	defer srv.Close()

	dir := t.TempDir()
	spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/terraria-server-1449.zip", TargetPath: "/terraria-server.zip"}
	if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err != nil {
		t.Fatal(err)
	}
	data, err := os.ReadFile(filepath.Join(dir, "terraria-server.zip"))
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(data, zipBuf.Bytes()) {
		t.Fatal("zip should have been written verbatim")
	}
}

// extract:true on non-archive content is a hard error, not a silent
// verbatim write.
func TestDirectExtractFlagOnNonArchiveErrors(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte("just text"))
	}))
	defer srv.Close()

	dir := t.TempDir()
	spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/blob", TargetPath: "/game", Extract: true}
	if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err == nil {
		t.Fatal("expected error for extract on non-archive content")
	}
}

// containerTargetAbs: absolute targets matching a declared root or
// volume mount stay container-absolute (factorio's /game volume);
// anything else keeps the historical BrowseableRoot join (terraria's
// /terraria-server.zip → /game-saves/terraria-server.zip).
func TestContainerTargetAbs(t *testing.T) {
	uc := &Context{
		BrowseableRoot: "/game-saves",
		Manifest: &modulepkg.Manifest{
			Runtime: modulepkg.Runtime{Docker: &modulepkg.DockerSpec{
				BrowseableRoots: []modulepkg.BrowseableRoot{{Name: "Saves", Path: "/game-saves"}},
				Volumes: []modulepkg.Volume{
					{Type: "volume", Name: "panel-$INSTANCE_ID-saves", Container: "/game-saves"},
					{Type: "volume", Name: "panel-$INSTANCE_ID-game", Container: "/game"},
				},
			}},
		},
	}
	cases := map[string]string{
		"/game":                "/game",
		"/game/sub":            "/game/sub",
		"/game-saves/mods":     "/game-saves/mods",
		"/terraria-server.zip": "/game-saves/terraria-server.zip",
		"relative/file":        "/game-saves/relative/file",
	}
	for in, want := range cases {
		if got := containerTargetAbs(uc, in); got != want {
			t.Errorf("containerTargetAbs(%q) = %q, want %q", in, got, want)
		}
	}
}

// Non-archive content with an extensionless target keeps the old
// single-file write behavior.
func TestDirectNonArchiveStillWritesFile(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte("binary-ish payload"))
	}))
	defer srv.Close()

	dir := t.TempDir()
	spec := &modulepkg.UpdateProvider{Kind: "direct", URL: srv.URL + "/blob", TargetPath: "/some/file"}
	if err := (DirectProvider{}).Update(context.Background(), testCtx(t, dir), spec); err != nil {
		t.Fatal(err)
	}
	data, err := os.ReadFile(filepath.Join(dir, "some", "file"))
	if err != nil {
		t.Fatal(err)
	}
	if string(data) != "binary-ish payload" {
		t.Fatalf("content mismatch: %q", data)
	}
}
