package web

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"testing"

	"gitea.dooplex.hu/admin/felhom-controller/internal/config"
	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)

// newSharingServer returns a Server with ONE registered storage root laid out like a real drive
// (appdata/, backups/, media/filmek, shares/) so the guard runs against a realistic tree.
func newSharingServer(t *testing.T) (*Server, string) {
	t.Helper()
	lg := log.New(io.Discard, "", 0)
	root := t.TempDir()
	drive := filepath.Join(root, "drive")
	for _, d := range []string{"appdata/paperless", "backups/unit", "media/filmek", "shares"} {
		if err := os.MkdirAll(filepath.Join(drive, filepath.FromSlash(d)), 0o755); err != nil {
			t.Fatal(err)
		}
	}
	sett, err := settings.Load(filepath.Join(root, "settings.json"), lg)
	if err != nil {
		t.Fatal(err)
	}
	if err := sett.AddStoragePath(settings.StoragePath{Path: drive, Label: "teszt", Schedulable: true}); err != nil {
		t.Fatal(err)
	}
	return &Server{settings: sett, logger: lg, cfg: &config.Config{}}, drive
}

// Scenario C: the picker/create guard matrix. Every refusal must ALSO be a non-effect.
func TestSharingResolvePath_GuardMatrix(t *testing.T) {
	s, drive := newSharingServer(t)
	outside := t.TempDir() // a real dir, but under no registered storage root

	refused := []struct{ name, path string }{
		{"appdata subtree (live app DB)", filepath.Join(drive, "appdata", "paperless")},
		{"appdata root", filepath.Join(drive, "appdata")},
		{"backups subtree", filepath.Join(drive, "backups", "unit")},
		{"the drive root itself", drive},
		{"outside every registered root", outside},
		{"relative path", "nem/abszolut"},
		{"empty", ""},
		{"traversal out of the root", filepath.Join(drive, "..", "escape")},
		{"nonexistent", filepath.Join(drive, "nincs-ilyen")},
	}
	for _, tc := range refused {
		if got, err := s.sharingResolvePath(tc.path); err == nil {
			t.Errorf("%s: must be refused, got %q", tc.name, got)
		}
	}

	// The guard is not "deny everything": a real user-data folder IS shareable (Scenario B shares
	// media/filmek). Without this the refusal tests above would pass on a broken always-deny guard.
	good := filepath.Join(drive, "media", "filmek")
	if _, err := s.sharingResolvePath(good); err != nil {
		t.Errorf("a user-data folder must be shareable, got %v", err)
	}
	if _, err := s.sharingResolvePath(filepath.Join(drive, "shares")); err != nil {
		t.Errorf("the shares dir must be shareable, got %v", err)
	}
}

// A symlink planted INSIDE a registered root that points OUTSIDE it must not escape the guard —
// this is why EvalSymlinks runs before the containment assert.
func TestSharingResolvePath_SymlinkEscapeRefused(t *testing.T) {
	s, drive := newSharingServer(t)
	outside := t.TempDir()
	link := filepath.Join(drive, "media", "escape-link")
	if err := os.Symlink(outside, link); err != nil {
		t.Skipf("symlinks unavailable on this platform/privilege level: %v", err)
	}
	if got, err := s.sharingResolvePath(link); err == nil {
		t.Errorf("a symlink escaping the storage root must be refused, resolved to %q", got)
	}
}

// A decommissioned storage root stops being shareable.
func TestSharingResolvePath_DecommissionedRootRefused(t *testing.T) {
	s, drive := newSharingServer(t)
	good := filepath.Join(drive, "media", "filmek")
	if _, err := s.sharingResolvePath(good); err != nil {
		t.Fatalf("precondition: %v", err)
	}
	if err := s.settings.SetDecommissioned(drive, ""); err != nil {
		t.Fatal(err)
	}
	if _, err := s.sharingResolvePath(good); err == nil {
		t.Error("a decommissioned storage root must not be shareable")
	}
}

// Every refusal returns the SAME message — a per-reason message would make the picker an oracle
// for the existence/contents of paths outside the customer's storage.
func TestSharingResolvePath_UniformRefusal(t *testing.T) {
	s, drive := newSharingServer(t)
	outside := t.TempDir()
	for _, p := range []string{
		filepath.Join(drive, "appdata"),
		outside,
		filepath.Join(drive, "nincs-ilyen"),
	} {
		_, err := s.sharingResolvePath(p)
		if err == nil {
			t.Fatalf("%s should refuse", p)
		}
		if err.Error() != errNotShareable.Error() {
			t.Errorf("refusal message must be uniform, got %q for %s", err.Error(), p)
		}
	}
}

// The "new folder" flow validates the storage ROOT, which the share-target guard rightly refuses.
// Regression guard for the live-validation bug: reusing sharingResolvePath there broke share
// creation entirely (the root is never a valid share TARGET, but is the valid PARENT).
func TestSharingResolveStorageRoot(t *testing.T) {
	s, drive := newSharingServer(t)

	// The registered root is accepted here...
	if got, err := s.sharingResolveStorageRoot(drive); err != nil || got == "" {
		t.Errorf("a registered storage root must be accepted as a new-folder parent, got %q err=%v", got, err)
	}
	// ...even though it is (correctly) refused as a share TARGET.
	if _, err := s.sharingResolvePath(drive); err == nil {
		t.Error("the drive root must still be refused as a share target")
	}

	// Anything that is not EXACTLY a registered live root is refused.
	for _, bad := range []string{
		filepath.Join(drive, "media"), // a subdir is not a root
		filepath.Join(drive, "appdata"),
		t.TempDir(), // unregistered
		"relative",
		"",
	} {
		if _, err := s.sharingResolveStorageRoot(bad); err == nil {
			t.Errorf("%q must not be accepted as a storage root", bad)
		}
	}

	// A decommissioned root stops being a valid parent.
	if err := s.settings.SetDecommissioned(drive, ""); err != nil {
		t.Fatal(err)
	}
	if _, err := s.sharingResolveStorageRoot(drive); err == nil {
		t.Error("a decommissioned root must not accept new folders")
	}
}

// pathWithin must be segment-wise: a sibling directory sharing a name PREFIX is not containment.
func TestPathWithin_SiblingPrefixIsNotContainment(t *testing.T) {
	if pathWithin("/mnt/drive-evil/x", "/mnt/drive") {
		t.Error("/mnt/drive-evil must NOT count as inside /mnt/drive")
	}
	if !pathWithin("/mnt/drive/x", "/mnt/drive") {
		t.Error("/mnt/drive/x must count as inside /mnt/drive")
	}
	if !pathWithin("/mnt/drive", "/mnt/drive") {
		t.Error("a root is within itself")
	}
}
