package static

import (
	"errors"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
)

func TestCopyDir_CopiesRegularFiles(t *testing.T) {
	src := t.TempDir()
	dst := filepath.Join(t.TempDir(), "out")

	if err := os.MkdirAll(filepath.Join(src, "sub"), 0o755); err != nil {
		t.Fatalf("mkdir sub: %v", err)
	}
	if err := os.WriteFile(filepath.Join(src, "index.html"), []byte("<h1>hi</h1>"), 0o644); err != nil {
		t.Fatalf("write index: %v", err)
	}
	if err := os.WriteFile(filepath.Join(src, "sub", "nested.txt"), []byte("nested"), 0o644); err != nil {
		t.Fatalf("write nested: %v", err)
	}

	if err := copyDir(src, dst); err != nil {
		t.Fatalf("copyDir: %v", err)
	}

	gotIndex, err := os.ReadFile(filepath.Join(dst, "index.html"))
	if err != nil {
		t.Fatalf("read index: %v", err)
	}
	if string(gotIndex) != "<h1>hi</h1>" {
		t.Errorf("index content = %q", string(gotIndex))
	}
	gotNested, err := os.ReadFile(filepath.Join(dst, "sub", "nested.txt"))
	if err != nil {
		t.Fatalf("read nested: %v", err)
	}
	if string(gotNested) != "nested" {
		t.Errorf("nested content = %q", string(gotNested))
	}
}

func TestCopyDir_PreservesFileMode(t *testing.T) {
	// File modes are only meaningful outside Windows — Windows reports
	// 0666 for any writable file regardless of the source mode.
	if runtime.GOOS == "windows" {
		t.Skip("file modes are not preserved meaningfully on Windows")
	}
	src := t.TempDir()
	dst := filepath.Join(t.TempDir(), "out")

	if err := os.WriteFile(filepath.Join(src, "script.sh"), []byte("#!/bin/sh\n"), 0o755); err != nil {
		t.Fatalf("write: %v", err)
	}

	if err := copyDir(src, dst); err != nil {
		t.Fatalf("copyDir: %v", err)
	}

	info, err := os.Stat(filepath.Join(dst, "script.sh"))
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if info.Mode().Perm() != 0o755 {
		t.Errorf("mode = %v, want 0755", info.Mode().Perm())
	}
}

func TestCopyDir_RejectsSymlinks(t *testing.T) {
	src := t.TempDir()
	dst := filepath.Join(t.TempDir(), "out")

	target := filepath.Join(src, "real.txt")
	if err := os.WriteFile(target, []byte("real"), 0o644); err != nil {
		t.Fatalf("write target: %v", err)
	}
	if err := os.Symlink(target, filepath.Join(src, "link.txt")); err != nil {
		// Windows non-admin users cannot create symlinks. The defense
		// is still valuable on Linux, so just skip when unsupported.
		t.Skipf("symlink not supported in this environment: %v", err)
	}

	err := copyDir(src, dst)
	if err == nil {
		t.Fatal("copyDir accepted a symlink; expected refusal")
	}
	if !strings.Contains(err.Error(), "non-regular") {
		t.Errorf("error = %v, want substring \"non-regular\"", err)
	}
}

func TestVerifyDownloadInsideRoot_AcceptsCleanTree(t *testing.T) {
	root := t.TempDir()
	if err := os.MkdirAll(filepath.Join(root, "sub"), 0o755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	if err := os.WriteFile(filepath.Join(root, "sub", "file.txt"), []byte("ok"), 0o644); err != nil {
		t.Fatalf("write: %v", err)
	}

	if err := verifyDownloadInsideRoot(root); err != nil {
		t.Fatalf("verifyDownloadInsideRoot rejected clean tree: %v", err)
	}
}

func TestVerifyDownloadInsideRoot_RejectsSymlink(t *testing.T) {
	root := t.TempDir()
	target := filepath.Join(t.TempDir(), "outside.txt")
	if err := os.WriteFile(target, []byte("outside"), 0o644); err != nil {
		t.Fatalf("write target: %v", err)
	}
	if err := os.Symlink(target, filepath.Join(root, "escape.txt")); err != nil {
		t.Skipf("symlink not supported in this environment: %v", err)
	}

	err := verifyDownloadInsideRoot(root)
	if err == nil {
		t.Fatal("verifyDownloadInsideRoot accepted symlink; expected refusal")
	}
	if !strings.Contains(err.Error(), "non-regular") {
		t.Errorf("error = %v, want substring \"non-regular\"", err)
	}
}

func TestVerifyDownloadInsideRoot_MissingRootSurfacesError(t *testing.T) {
	err := verifyDownloadInsideRoot(filepath.Join(t.TempDir(), "does-not-exist"))
	if err == nil {
		t.Fatal("expected error for missing root, got nil")
	}
	if !errors.Is(err, os.ErrNotExist) {
		t.Errorf("error = %v, want os.ErrNotExist in chain", err)
	}
}

func TestPrepareStaticBuild_WritesDockerfileAndCopiesFiles(t *testing.T) {
	src := t.TempDir()
	ctxDir := filepath.Join(t.TempDir(), "ctx")

	if err := os.WriteFile(filepath.Join(src, "index.html"), []byte("hello"), 0o644); err != nil {
		t.Fatalf("write index: %v", err)
	}

	if err := prepareStaticBuild(src, ctxDir); err != nil {
		t.Fatalf("prepareStaticBuild: %v", err)
	}

	if _, err := os.Stat(filepath.Join(ctxDir, "Dockerfile")); err != nil {
		t.Fatalf("Dockerfile missing: %v", err)
	}
	got, err := os.ReadFile(filepath.Join(ctxDir, "index.html"))
	if err != nil {
		t.Fatalf("read copied index: %v", err)
	}
	if string(got) != "hello" {
		t.Errorf("copied index content = %q", string(got))
	}
}

func TestPrepareStaticBuild_EmptySrcStillWritesDockerfile(t *testing.T) {
	// An empty repo folder shouldn't crash the build — nginx will just
	// serve an empty image. The Dockerfile must still land.
	src := t.TempDir()
	ctxDir := filepath.Join(t.TempDir(), "ctx")

	if err := prepareStaticBuild(src, ctxDir); err != nil {
		t.Fatalf("prepareStaticBuild: %v", err)
	}
	if _, err := os.Stat(filepath.Join(ctxDir, "Dockerfile")); err != nil {
		t.Fatalf("Dockerfile missing: %v", err)
	}
}
