package fileio

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

func TestSafePathRejectsUnsafeNames(t *testing.T) {
	for _, name := range []string{"/tmp/x", "a/../x", "a//x", `a\x`, `a\\x`} {
		if _, err := SafePath(t.TempDir(), name); err == nil {
			t.Fatalf("SafePath(%q) accepted unsafe path", name)
		}
	}
}

func TestEncodePathComponentIsInjectiveAndSafe(t *testing.T) {
	root := t.TempDir()
	seen := make(map[string]string)
	for _, test := range []struct {
		value string
		want  string
	}{
		{value: "", want: "%"},
		{value: ".", want: "%2E"},
		{value: "..", want: "%2E%2E"},
		{value: "_", want: "_"},
		{value: "a..b", want: "a%2E%2Eb"},
		{value: "safe.identifier-9", want: "safe.identifier-9"},
		{value: "left/right", want: "left%2Fright"},
		{value: "%", want: "%25"},
		{value: "~", want: "%7E"},
		{value: " a ", want: "%20a%20"},
		{value: "é", want: "%C3%A9"},
	} {
		got := EncodePathComponent(test.value)
		if got != test.want {
			t.Errorf("EncodePathComponent(%q) = %q, want %q", test.value, got, test.want)
		}
		if previous, ok := seen[got]; ok {
			t.Errorf("EncodePathComponent(%q) = %q, collides with %q", test.value, got, previous)
		}
		seen[got] = test.value
		if _, err := SafePath(root, "components/"+got); err != nil {
			t.Errorf("EncodePathComponent(%q) produced unsafe component %q: %v", test.value, got, err)
		}
	}
}

func TestWriteBytesIsAtomicAndUsesRequestedModes(t *testing.T) {
	root := t.TempDir()
	if err := WriteBytes(root, "nested/value", []byte("value"), 0o700, 0o600); err != nil {
		t.Fatal(err)
	}
	for path, want := range map[string]os.FileMode{filepath.Join(root, "nested"): 0o700, filepath.Join(root, "nested", "value"): 0o600} {
		info, err := os.Stat(path)
		if err != nil {
			t.Fatal(err)
		}
		if info.Mode().Perm() != want {
			t.Fatalf("%s mode=%#o want %#o", path, info.Mode().Perm(), want)
		}
	}
	entries, err := os.ReadDir(filepath.Join(root, "nested"))
	if err != nil {
		t.Fatal(err)
	}
	for _, entry := range entries {
		if strings.Contains(entry.Name(), ".tmp-") {
			t.Fatalf("temporary file remains: %s", entry.Name())
		}
	}
}

func TestWriteBytesRejectsSymlinkedComponents(t *testing.T) {
	root := t.TempDir()
	outside := t.TempDir()
	if err := os.Symlink(outside, filepath.Join(root, "link")); err != nil {
		t.Skipf("symbolic links unavailable: %v", err)
	}

	if err := WriteBytes(root, "link/value", []byte("value"), 0o700, 0o600); err == nil {
		t.Fatal("WriteBytes accepted a symlinked directory")
	}
	if _, err := os.Stat(filepath.Join(outside, "value")); !os.IsNotExist(err) {
		t.Fatalf("write escaped through symlink: %v", err)
	}
}
