package appexport

import (
	"encoding/json"
	"strings"
	"testing"
)

// Regression test for [CTRL-001] (path traversal on .fab import). Originated as
// a failing audit test (audit/2026-06-13-deep-sweep); now a permanent guard.
// UnmarshalManifest must REJECT any manifest whose AppName / HDDSubdirs /
// VolumeNames contain a path-traversal or separator, and ACCEPT legitimate
// single-segment names. Do NOT weaken these assertions.

func mustManifestJSON(t *testing.T, m Manifest) []byte {
	t.Helper()
	b, err := json.Marshal(m)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	return b
}

func TestUnmarshalManifestRejectsTraversal(t *testing.T) {
	cases := []struct {
		name string
		m    Manifest
	}{
		{"appname-parent", Manifest{Version: 1, AppName: "../evil"}},
		{"appname-deep", Manifest{Version: 1, AppName: "../../etc/cron.d/x"}},
		{"appname-absolute", Manifest{Version: 1, AppName: "/etc/cron.d/x"}},
		{"appname-dotdot", Manifest{Version: 1, AppName: ".."}},
		{"appname-empty", Manifest{Version: 1, AppName: ""}},
		{"appname-backslash", Manifest{Version: 1, AppName: `..\evil`}},
		{"hdd-subdir-escape", Manifest{Version: 1, AppName: "romm", HDDSubdirs: []string{"../../mnt"}}},
		{"volume-escape", Manifest{Version: 1, AppName: "romm", VolumeNames: []string{"../../var/lib"}}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, err := UnmarshalManifest(mustManifestJSON(t, tc.m))
			if err == nil {
				t.Fatalf("CTRL-001 regression: UnmarshalManifest accepted a traversal manifest %+v; expected rejection", tc.m)
			}
		})
	}
}

func TestUnmarshalManifestAcceptsLegitNames(t *testing.T) {
	m := Manifest{
		Version:     1,
		AppName:     "paperless-ngx",
		HDDSubdirs:  []string{"felhom-usb", "romm"},
		VolumeNames: []string{"adventurelog_postgres_data", "romm_redis-data"},
		ConfigFiles: []string{".felhom.yml", "docker-compose.yml", "app.yaml"}, // dotfiles must NOT be rejected
	}
	got, err := UnmarshalManifest(mustManifestJSON(t, m))
	if err != nil {
		t.Fatalf("CTRL-001 regression: UnmarshalManifest rejected a legitimate manifest: %v", err)
	}
	if got.AppName != "paperless-ngx" {
		t.Fatalf("AppName round-trip mismatch: %q", got.AppName)
	}
}

func TestValidateSegment(t *testing.T) {
	good := []string{"romm", "paperless-ngx", "adventurelog_postgres_data", "felhom-usb", "a", "App1.2_3-4"}
	for _, s := range good {
		if err := ValidateSegment("x", s); err != nil {
			t.Errorf("ValidateSegment(%q) = %v; want nil", s, err)
		}
	}
	bad := []string{"", ".", "..", "../x", "a/b", `a\b`, "/abs", ".hidden", "-leadingdash", "a/../b"}
	for _, s := range bad {
		if err := ValidateSegment("x", s); err == nil {
			t.Errorf("ValidateSegment(%q) = nil; want rejection", s)
		}
	}
	// Sanity: a rejected value's message names the kind, for operator clarity.
	if err := ValidateSegment("app_name", "../x"); err == nil || !strings.Contains(err.Error(), "app_name") {
		t.Errorf("expected error mentioning app_name, got %v", err)
	}
}
