package http

import (
	"context"
	"io"
	stdhttp "net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"gitea.dcglab.co.uk/steve/restic-manager/internal/crypto"
	"gitea.dcglab.co.uk/steve/restic-manager/internal/server/config"
	"gitea.dcglab.co.uk/steve/restic-manager/internal/server/ws"
	"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
)

// newAssetsTestServer is a minimal scaffold for the /agent/binary and
// /install/* handlers. Two roots: one acts as DataDir, the other as
// the image-baked BundledAssetsDir. Either or both may be empty.
func newAssetsTestServer(t *testing.T, populate func(dataDir, bundleDir string)) string {
	t.Helper()
	root := t.TempDir()
	dataDir := filepath.Join(root, "data")
	bundleDir := filepath.Join(root, "dist")
	for _, d := range []string{
		filepath.Join(dataDir, "agent-binaries"),
		filepath.Join(dataDir, "install"),
		filepath.Join(bundleDir, "agent-binaries"),
		filepath.Join(bundleDir, "install"),
	} {
		if err := os.MkdirAll(d, 0o755); err != nil {
			t.Fatalf("mkdir: %v", err)
		}
	}
	if populate != nil {
		populate(dataDir, bundleDir)
	}

	st, err := store.Open(context.Background(), filepath.Join(root, "rm.db"))
	if err != nil {
		t.Fatalf("store: %v", err)
	}
	t.Cleanup(func() { _ = st.Close() })

	keyPath := filepath.Join(root, "secret.key")
	_ = crypto.GenerateKeyFile(keyPath)
	key, _ := crypto.LoadKeyFromFile(keyPath)
	aead, _ := crypto.NewAEAD(key)

	deps := Deps{
		Cfg: config.Config{
			Listen:           ":0",
			DataDir:          dataDir,
			SecretKeyFile:    keyPath,
			BundledAssetsDir: bundleDir,
		},
		Store:          st,
		AEAD:           aead,
		Hub:            ws.NewHub(),
		BootstrapToken: "test-token",
	}
	s := New(deps)
	ts := httptest.NewServer(s.srv.Handler)
	t.Cleanup(ts.Close)
	return ts.URL
}

func writeFile(t *testing.T, path string, body []byte) {
	t.Helper()
	if err := os.WriteFile(path, body, 0o644); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
}

func get(t *testing.T, url string) (int, []byte) {
	t.Helper()
	res, err := stdhttp.Get(url)
	if err != nil {
		t.Fatalf("GET %s: %v", url, err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	return res.StatusCode, body
}

func TestAgentBinary_DataDirHit(t *testing.T) {
	t.Parallel()
	url := newAssetsTestServer(t, func(dataDir, _ string) {
		writeFile(t, filepath.Join(dataDir, "agent-binaries", "restic-manager-agent-linux-amd64"),
			[]byte("from-datadir"))
	})
	code, body := get(t, url+"/agent/binary?os=linux&arch=amd64")
	if code != 200 || string(body) != "from-datadir" {
		t.Fatalf("got %d %q", code, string(body))
	}
}

func TestAgentBinary_BundleFallback(t *testing.T) {
	t.Parallel()
	url := newAssetsTestServer(t, func(_, bundleDir string) {
		writeFile(t, filepath.Join(bundleDir, "agent-binaries", "restic-manager-agent-linux-amd64"),
			[]byte("from-bundle"))
	})
	code, body := get(t, url+"/agent/binary?os=linux&arch=amd64")
	if code != 200 || string(body) != "from-bundle" {
		t.Fatalf("got %d %q", code, string(body))
	}
}

func TestAgentBinary_DataDirShadowsBundle(t *testing.T) {
	t.Parallel()
	url := newAssetsTestServer(t, func(dataDir, bundleDir string) {
		writeFile(t, filepath.Join(dataDir, "agent-binaries", "restic-manager-agent-linux-amd64"),
			[]byte("from-datadir"))
		writeFile(t, filepath.Join(bundleDir, "agent-binaries", "restic-manager-agent-linux-amd64"),
			[]byte("from-bundle"))
	})
	code, body := get(t, url+"/agent/binary?os=linux&arch=amd64")
	if code != 200 || string(body) != "from-datadir" {
		t.Fatalf("operator override should win: got %d %q", code, string(body))
	}
}

func TestAgentBinary_BothMiss(t *testing.T) {
	t.Parallel()
	url := newAssetsTestServer(t, nil)
	code, _ := get(t, url+"/agent/binary?os=linux&arch=amd64")
	if code != 404 {
		t.Fatalf("expected 404, got %d", code)
	}
}

func TestAgentBinary_WindowsNameHasExe(t *testing.T) {
	t.Parallel()
	url := newAssetsTestServer(t, func(_, bundleDir string) {
		writeFile(t, filepath.Join(bundleDir, "agent-binaries", "restic-manager-agent-windows-amd64.exe"),
			[]byte("win"))
	})
	code, body := get(t, url+"/agent/binary?os=windows&arch=amd64")
	if code != 200 || string(body) != "win" {
		t.Fatalf("got %d %q", code, string(body))
	}
}

func TestInstallAsset_BundleFallback(t *testing.T) {
	t.Parallel()
	url := newAssetsTestServer(t, func(_, bundleDir string) {
		writeFile(t, filepath.Join(bundleDir, "install", "install.sh"), []byte("#!/bin/sh\n"))
	})
	code, body := get(t, url+"/install/install.sh")
	if code != 200 || string(body) != "#!/bin/sh\n" {
		t.Fatalf("got %d %q", code, string(body))
	}
}

func TestInstallAsset_PathTraversalRejected(t *testing.T) {
	t.Parallel()
	url := newAssetsTestServer(t, nil)
	// chi will normalise some traversal attempts, but the handler
	// also rejects any rel containing a slash or backslash. The
	// path component of the URL after /install/ is the rel.
	code, _ := get(t, url+"/install/..%2fpasswd")
	if code == 200 {
		t.Fatalf("traversal should not return 200")
	}
}
