package appbackup

import (
	"reflect"
	"testing"
)

func bucketAbs(b []CapturePath) []string {
	out := make([]string, 0, len(b))
	for _, p := range b {
		out = append(out, p.Abs)
	}
	return out
}

// classified app → three buckets, resolved + Abs-sorted.
func TestComputeFabBuckets_Classified(t *testing.T) {
	binds := []ClassifiedBind{
		{ComposeBind: ComposeBind{Root: RootUserdata, RelPath: "media/books"}, Class: ClassMandatory},
		{ComposeBind: ComposeBind{Root: RootUserdata, RelPath: "media/comics"}, Class: ClassOptional},
		{ComposeBind: ComposeBind{Root: RootUserdata, RelPath: "media/movies"}, Class: ClassExcluded},
		{ComposeBind: ComposeBind{Root: RootHDD, RelPath: "appdata/app"}, Class: ClassMandatory},
	}
	fb := ComputeFabBuckets(binds, true, drv)
	if !fb.HasClassification {
		t.Fatal("HasClassification must be true")
	}
	if got, want := bucketAbs(fb.Mandatory), []string{hdd("appdata/app"), udat("media/books")}; !reflect.DeepEqual(got, want) {
		t.Errorf("Mandatory = %v, want %v", got, want)
	}
	if got, want := bucketAbs(fb.Optional), []string{udat("media/comics")}; !reflect.DeepEqual(got, want) {
		t.Errorf("Optional = %v, want %v", got, want)
	}
	if got, want := bucketAbs(fb.Excluded), []string{udat("media/movies")}; !reflect.DeepEqual(got, want) {
		t.Errorf("Excluded = %v, want %v", got, want)
	}
}

// legacy (no block) → empty buckets (the full-root capture stays out of the classified plan).
func TestComputeFabBuckets_LegacyEmpty(t *testing.T) {
	binds := []ClassifiedBind{{ComposeBind: ComposeBind{Root: RootUserdata, RelPath: "media/tv"}, Origin: OriginLegacy}}
	fb := ComputeFabBuckets(binds, false, drv)
	if fb.HasClassification || fb.Mandatory != nil || fb.Optional != nil || fb.Excluded != nil {
		t.Errorf("legacy app must yield empty buckets, got %+v", fb)
	}
}

// Scenario E: structural guards run over ALL classes — a traversal path in an EXCLUDED bind is Skipped,
// never plannable (opt-in or not).
func TestComputeFabBuckets_GuardsAllClasses(t *testing.T) {
	binds := []ClassifiedBind{
		{ComposeBind: ComposeBind{Root: RootHDD, RelPath: "../evil"}, Class: ClassExcluded},
		{ComposeBind: ComposeBind{Root: RootHDD, RelPath: "appdata/ok"}, Class: ClassMandatory},
	}
	fb := ComputeFabBuckets(binds, true, drv)
	for _, b := range [][]CapturePath{fb.Mandatory, fb.Optional, fb.Excluded} {
		for _, p := range b {
			if p.RelPath == "../evil" {
				t.Fatal("a traversal path must never enter a bucket (guards run over all classes)")
			}
		}
	}
	if len(fb.Skipped) != 1 || fb.Skipped[0].RelPath != "../evil" {
		t.Errorf("traversal excluded path must be Skipped, got %+v", fb.Skipped)
	}
}

// no cross-bucket containment dedup: a mandatory CHILD inside an excluded PARENT both survive.
func TestComputeFabBuckets_NoCrossBucketContainment(t *testing.T) {
	binds := []ClassifiedBind{
		{ComposeBind: ComposeBind{Root: RootUserdata, RelPath: "media"}, Class: ClassExcluded},
		{ComposeBind: ComposeBind{Root: RootUserdata, RelPath: "media/books"}, Class: ClassMandatory},
	}
	fb := ComputeFabBuckets(binds, true, drv)
	if got, want := bucketAbs(fb.Mandatory), []string{udat("media/books")}; !reflect.DeepEqual(got, want) {
		t.Errorf("mandatory child must survive independently: Mandatory = %v, want %v", got, want)
	}
	if got, want := bucketAbs(fb.Excluded), []string{udat("media")}; !reflect.DeepEqual(got, want) {
		t.Errorf("excluded parent must survive: Excluded = %v, want %v", got, want)
	}
}

// equal-Abs collapse: two spellings of one path collapse, mandatory wins (into the mandatory bucket).
func TestComputeFabBuckets_EqualAbsMandatoryWins(t *testing.T) {
	binds := []ClassifiedBind{
		{ComposeBind: ComposeBind{Root: RootHDD, RelPath: "userdata/media"}, Class: ClassOptional},
		{ComposeBind: ComposeBind{Root: RootUserdata, RelPath: "media"}, Class: ClassMandatory},
	}
	fb := ComputeFabBuckets(binds, true, drv)
	if got, want := bucketAbs(fb.Mandatory), []string{udat("media")}; !reflect.DeepEqual(got, want) {
		t.Errorf("collapsed path must land in Mandatory, got Mandatory=%v", got)
	}
	if len(fb.Optional) != 0 {
		t.Errorf("optional spelling must collapse away, got %v", bucketAbs(fb.Optional))
	}
}
