package redfishprofile

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

func TestBuildAcquisitionPlan_Fixture_MSI_CG480(t *testing.T) {
	signals := loadProfileFixtureSignals(t, "msi-cg480.json")
	match := MatchProfiles(signals)
	plan := BuildAcquisitionPlan(signals)
	resolved := ResolveAcquisitionPlan(match, plan, discoveredResourcesFromSignals(signals), signals)

	if match.Mode != ModeMatched {
		t.Fatalf("expected matched mode, got %q", match.Mode)
	}
	assertProfileSelected(t, match, "msi")
	assertProfileSelected(t, match, "ami-family")
	assertProfileNotSelected(t, match, "hgx-topology")

	if plan.Tuning.PrefetchWorkers < 6 {
		t.Fatalf("expected msi prefetch worker tuning, got %d", plan.Tuning.PrefetchWorkers)
	}
	if !containsString(resolved.SeedPaths, "/redfish/v1/Chassis/GPU1") {
		t.Fatalf("expected MSI chassis GPU seed path")
	}
	if !containsString(resolved.CriticalPaths, "/redfish/v1/Chassis/GPU1/Sensors") {
		t.Fatal("expected MSI GPU sensor critical path")
	}
	if !containsString(resolved.Plan.PlanBPaths, "/redfish/v1/Chassis/GPU1/Sensors") {
		t.Fatal("expected MSI GPU sensor plan-b path")
	}
	if plan.Tuning.ETABaseline.SnapshotSeconds <= 0 {
		t.Fatal("expected MSI snapshot eta baseline")
	}
	if !plan.Tuning.PostProbePolicy.EnableNumericCollectionProbe {
		t.Fatal("expected MSI fixture to inherit generic numeric post-probe policy")
	}
	if !containsString(plan.ScopedPaths.SystemSeedSuffixes, "/SimpleStorage") {
		t.Fatal("expected MSI fixture to inherit generic SimpleStorage scoped seed suffix")
	}
	if !containsString(plan.ScopedPaths.SystemCriticalSuffixes, "/Memory") {
		t.Fatal("expected MSI fixture to inherit generic system critical suffixes")
	}
	if !containsString(plan.Tuning.PrefetchPolicy.IncludeSuffixes, "/Storage") {
		t.Fatal("expected MSI fixture to inherit generic storage prefetch policy")
	}
	if !containsString(plan.CriticalPaths, "/redfish/v1/UpdateService") {
		t.Fatal("expected MSI fixture to inherit generic top-level critical path")
	}
	if !plan.Tuning.RecoveryPolicy.EnableProfilePlanB {
		t.Fatal("expected MSI fixture to enable profile plan-b")
	}
}

func TestBuildAcquisitionPlan_Fixture_MSI_CG480_CopyMatchesSameProfiles(t *testing.T) {
	originalSignals := loadProfileFixtureSignals(t, "msi-cg480.json")
	copySignals := loadProfileFixtureSignals(t, "msi-cg480-copy.json")
	originalMatch := MatchProfiles(originalSignals)
	copyMatch := MatchProfiles(copySignals)
	originalPlan := BuildAcquisitionPlan(originalSignals)
	copyPlan := BuildAcquisitionPlan(copySignals)
	originalResolved := ResolveAcquisitionPlan(originalMatch, originalPlan, discoveredResourcesFromSignals(originalSignals), originalSignals)
	copyResolved := ResolveAcquisitionPlan(copyMatch, copyPlan, discoveredResourcesFromSignals(copySignals), copySignals)

	assertSameProfileNames(t, originalMatch, copyMatch)
	if originalPlan.Tuning.PrefetchWorkers != copyPlan.Tuning.PrefetchWorkers {
		t.Fatalf("expected same MSI prefetch worker tuning, got %d vs %d", originalPlan.Tuning.PrefetchWorkers, copyPlan.Tuning.PrefetchWorkers)
	}
	if containsString(originalResolved.SeedPaths, "/redfish/v1/Chassis/GPU1") != containsString(copyResolved.SeedPaths, "/redfish/v1/Chassis/GPU1") {
		t.Fatal("expected same MSI GPU chassis seed presence in both fixtures")
	}
}

func TestBuildAcquisitionPlan_Fixture_MSI_CG290(t *testing.T) {
	signals := loadProfileFixtureSignals(t, "msi-cg290.json")
	match := MatchProfiles(signals)
	plan := BuildAcquisitionPlan(signals)
	resolved := ResolveAcquisitionPlan(match, plan, discoveredResourcesFromSignals(signals), signals)

	if match.Mode != ModeMatched {
		t.Fatalf("expected matched mode, got %q", match.Mode)
	}
	assertProfileSelected(t, match, "msi")
	assertProfileSelected(t, match, "ami-family")
	assertProfileNotSelected(t, match, "hgx-topology")

	if plan.Tuning.PrefetchWorkers < 6 {
		t.Fatalf("expected MSI prefetch worker tuning, got %d", plan.Tuning.PrefetchWorkers)
	}
	if !containsString(resolved.SeedPaths, "/redfish/v1/Chassis/GPU1") {
		t.Fatalf("expected MSI chassis GPU seed path")
	}
}

func TestBuildAcquisitionPlan_Fixture_Supermicro_HGX(t *testing.T) {
	signals := loadProfileFixtureSignals(t, "supermicro-hgx.json")
	match := MatchProfiles(signals)
	plan := BuildAcquisitionPlan(signals)
	discovered := discoveredResourcesFromSignals(signals)
	discovered.SystemPaths = dedupeSorted(append(discovered.SystemPaths, "/redfish/v1/Systems/HGX_Baseboard_0"))
	resolved := ResolveAcquisitionPlan(match, plan, discovered, signals)

	if match.Mode != ModeMatched {
		t.Fatalf("expected matched mode, got %q", match.Mode)
	}
	assertProfileSelected(t, match, "supermicro")
	assertProfileSelected(t, match, "hgx-topology")
	assertProfileNotSelected(t, match, "msi")

	if plan.Tuning.SnapshotMaxDocuments < 180000 {
		t.Fatalf("expected widened HGX snapshot cap, got %d", plan.Tuning.SnapshotMaxDocuments)
	}
	if plan.Tuning.NVMePostProbeEnabled == nil || *plan.Tuning.NVMePostProbeEnabled {
		t.Fatal("expected HGX fixture to disable NVMe post-probe")
	}
	if !containsString(resolved.SeedPaths, "/redfish/v1/Systems/HGX_Baseboard_0/Processors") {
		t.Fatal("expected HGX baseboard processors seed path")
	}
	if !containsString(resolved.CriticalPaths, "/redfish/v1/Systems/HGX_Baseboard_0/Processors") {
		t.Fatal("expected HGX baseboard processors critical path")
	}
	if !containsString(resolved.Plan.PlanBPaths, "/redfish/v1/Systems/HGX_Baseboard_0/Processors") {
		t.Fatal("expected HGX baseboard processors plan-b path")
	}
	if plan.Tuning.ETABaseline.SnapshotSeconds < 300 {
		t.Fatalf("expected HGX snapshot eta baseline, got %d", plan.Tuning.ETABaseline.SnapshotSeconds)
	}
	if !plan.Tuning.PostProbePolicy.EnableDirectNVMEDiskBayProbe {
		t.Fatal("expected HGX fixture to retain Supermicro direct NVMe disk bay probe policy")
	}
	if !containsString(plan.ScopedPaths.SystemCriticalSuffixes, "/Storage/IntelVROC/Drives") {
		t.Fatal("expected HGX fixture to inherit generic IntelVROC scoped critical suffix")
	}
	if !containsString(plan.ScopedPaths.ChassisCriticalSuffixes, "/Assembly") {
		t.Fatal("expected HGX fixture to inherit generic chassis critical suffixes")
	}
	if !containsString(plan.Tuning.PrefetchPolicy.ExcludeContains, "/Assembly") {
		t.Fatal("expected HGX fixture to inherit generic assembly prefetch exclusion")
	}
	if !plan.Tuning.RecoveryPolicy.EnableProfilePlanB {
		t.Fatal("expected HGX fixture to enable profile plan-b")
	}
}

func TestBuildAcquisitionPlan_Fixture_Supermicro_OAM_NoHGX(t *testing.T) {
	signals := loadProfileFixtureSignals(t, "supermicro-oam-amd.json")
	match := MatchProfiles(signals)
	plan := BuildAcquisitionPlan(signals)
	resolved := ResolveAcquisitionPlan(match, plan, discoveredResourcesFromSignals(signals), signals)

	if match.Mode != ModeMatched {
		t.Fatalf("expected matched mode, got %q", match.Mode)
	}
	assertProfileSelected(t, match, "supermicro")
	assertProfileNotSelected(t, match, "hgx-topology")
	assertProfileNotSelected(t, match, "msi")

	if containsString(resolved.SeedPaths, "/redfish/v1/Systems/HGX_Baseboard_0/Processors") {
		t.Fatal("did not expect HGX baseboard processors seed path for OAM fixture")
	}
	if containsString(resolved.CriticalPaths, "/redfish/v1/Systems/HGX_Baseboard_0/Processors") {
		t.Fatal("did not expect HGX baseboard processors critical path for OAM fixture")
	}
	if !containsString(resolved.CriticalPaths, "/redfish/v1/UpdateService/Oem/Supermicro/FirmwareInventory") {
		t.Fatal("expected Supermicro firmware critical path")
	}
	if !containsString(resolved.Plan.PlanBPaths, "/redfish/v1/UpdateService/Oem/Supermicro/FirmwareInventory") {
		t.Fatal("expected Supermicro firmware plan-b path")
	}
	if plan.Tuning.SnapshotMaxDocuments != 150000 {
		t.Fatalf("expected generic supermicro snapshot cap, got %d", plan.Tuning.SnapshotMaxDocuments)
	}
	if plan.Tuning.NVMePostProbeEnabled != nil {
		t.Fatal("did not expect HGX NVMe tuning for OAM fixture")
	}
	if plan.Tuning.ETABaseline.SnapshotSeconds < 180 {
		t.Fatalf("expected Supermicro snapshot eta baseline, got %d", plan.Tuning.ETABaseline.SnapshotSeconds)
	}
	if !plan.Tuning.PostProbePolicy.EnableDirectNVMEDiskBayProbe {
		t.Fatal("expected Supermicro OAM fixture to use direct NVMe disk bay probe policy")
	}
	if !plan.Tuning.PostProbePolicy.EnableNumericCollectionProbe {
		t.Fatal("expected Supermicro OAM fixture to inherit generic numeric post-probe policy")
	}
	if !containsString(plan.ScopedPaths.SystemSeedSuffixes, "/Storage/IntelVROC") {
		t.Fatal("expected Supermicro OAM fixture to inherit generic IntelVROC scoped seed suffix")
	}
	if !plan.Tuning.RecoveryPolicy.EnableProfilePlanB {
		t.Fatal("expected Supermicro OAM fixture to enable profile plan-b")
	}
}

func TestBuildAcquisitionPlan_Fixture_Dell_R750(t *testing.T) {
	signals := loadProfileFixtureSignals(t, "dell-r750.json")
	match := MatchProfiles(signals)
	plan := BuildAcquisitionPlan(signals)
	resolved := ResolveAcquisitionPlan(match, plan, DiscoveredResources{
		SystemPaths:  []string{"/redfish/v1/Systems/System.Embedded.1"},
		ChassisPaths: []string{"/redfish/v1/Chassis/System.Embedded.1"},
		ManagerPaths: []string{"/redfish/v1/Managers/1", "/redfish/v1/Managers/iDRAC.Embedded.1"},
	}, signals)

	if match.Mode != ModeMatched {
		t.Fatalf("expected matched mode, got %q", match.Mode)
	}
	assertProfileSelected(t, match, "dell")
	assertProfileNotSelected(t, match, "supermicro")
	assertProfileNotSelected(t, match, "hgx-topology")
	assertProfileNotSelected(t, match, "msi")

	if !plan.Tuning.RecoveryPolicy.EnableProfilePlanB {
		t.Fatal("expected dell fixture to enable profile plan-b")
	}
	if !containsString(resolved.SeedPaths, "/redfish/v1/Managers/iDRAC.Embedded.1") {
		t.Fatal("expected Dell refinement to add iDRAC manager seed path")
	}
	if !containsString(resolved.CriticalPaths, "/redfish/v1/Managers/iDRAC.Embedded.1") {
		t.Fatal("expected Dell refinement to add iDRAC manager critical path")
	}
	directives := ResolveAnalysisPlan(match, nil, DiscoveredResources{}, signals).Directives
	if !directives.EnableGenericGraphicsControllerDedup {
		t.Fatal("expected dell fixture to enable graphics controller dedup")
	}
}

func TestBuildAcquisitionPlan_Fixture_AMI_Generic(t *testing.T) {
	signals := loadProfileFixtureSignals(t, "ami-generic.json")
	match := MatchProfiles(signals)
	plan := BuildAcquisitionPlan(signals)

	if match.Mode != ModeMatched {
		t.Fatalf("expected matched mode, got %q", match.Mode)
	}
	assertProfileSelected(t, match, "ami-family")
	assertProfileNotSelected(t, match, "msi")
	assertProfileNotSelected(t, match, "supermicro")
	assertProfileNotSelected(t, match, "dell")
	assertProfileNotSelected(t, match, "hgx-topology")

	if plan.Tuning.PrefetchEnabled == nil || !*plan.Tuning.PrefetchEnabled {
		t.Fatal("expected ami-family fixture to force prefetch enabled")
	}
	if !containsString(plan.SeedPaths, "/redfish/v1/Oem/Ami") {
		t.Fatal("expected ami-family fixture seed path /redfish/v1/Oem/Ami")
	}
	if !containsString(plan.SeedPaths, "/redfish/v1/Oem/Ami/InventoryData/Status") {
		t.Fatal("expected ami-family fixture seed path /redfish/v1/Oem/Ami/InventoryData/Status")
	}
	if !containsString(plan.CriticalPaths, "/redfish/v1/UpdateService") {
		t.Fatal("expected ami-family fixture to inherit generic critical path")
	}

	directives := ResolveAnalysisPlan(match, nil, DiscoveredResources{}, signals).Directives
	if !directives.EnableGenericGraphicsControllerDedup {
		t.Fatal("expected ami-family fixture to enable graphics controller dedup")
	}
}

func TestBuildAcquisitionPlan_Fixture_UnknownVendor(t *testing.T) {
	signals := loadProfileFixtureSignals(t, "unknown-vendor.json")
	match := MatchProfiles(signals)
	plan := BuildAcquisitionPlan(signals)
	resolved := ResolveAcquisitionPlan(match, plan, DiscoveredResources{
		SystemPaths:  []string{"/redfish/v1/Systems/1"},
		ChassisPaths: []string{"/redfish/v1/Chassis/1"},
		ManagerPaths: []string{"/redfish/v1/Managers/1"},
	}, signals)

	if match.Mode != ModeFallback {
		t.Fatalf("expected fallback mode for unknown vendor, got %q", match.Mode)
	}
	if len(match.Profiles) == 0 {
		t.Fatal("expected fallback to aggregate profiles")
	}
	for _, profile := range match.Profiles {
		if !profile.SafeForFallback() {
			t.Fatalf("fallback mode included non-safe profile %q", profile.Name())
		}
	}

	if plan.Tuning.SnapshotMaxDocuments < 180000 {
		t.Fatalf("expected fallback to widen snapshot cap, got %d", plan.Tuning.SnapshotMaxDocuments)
	}
	if plan.Tuning.PrefetchEnabled == nil || !*plan.Tuning.PrefetchEnabled {
		t.Fatal("expected fallback fixture to force prefetch enabled")
	}
	if !containsString(resolved.CriticalPaths, "/redfish/v1/Systems/1") {
		t.Fatal("expected fallback resolved critical paths to include discovered system")
	}

	analysisPlan := ResolveAnalysisPlan(match, nil, DiscoveredResources{}, signals)
	if !analysisPlan.Directives.EnableProcessorGPUFallback {
		t.Fatal("expected fallback fixture to enable processor GPU fallback")
	}
	if !analysisPlan.Directives.EnableStorageEnclosureRecovery {
		t.Fatal("expected fallback fixture to enable storage enclosure recovery")
	}
	if !analysisPlan.Directives.EnableGenericGraphicsControllerDedup {
		t.Fatal("expected fallback fixture to enable graphics controller dedup")
	}
}

func TestBuildAcquisitionPlan_Fixture_xFusion_G5500V7(t *testing.T) {
	signals := loadProfileFixtureSignals(t, "xfusion-g5500v7.json")
	match := MatchProfiles(signals)
	plan := BuildAcquisitionPlan(signals)
	resolved := ResolveAcquisitionPlan(match, plan, DiscoveredResources{
		SystemPaths:  []string{"/redfish/v1/Systems/1"},
		ChassisPaths: []string{"/redfish/v1/Chassis/1"},
		ManagerPaths: []string{"/redfish/v1/Managers/1"},
	}, signals)

	if match.Mode != ModeMatched {
		t.Fatalf("expected matched mode for xFusion, got %q", match.Mode)
	}
	assertProfileSelected(t, match, "xfusion")
	assertProfileNotSelected(t, match, "supermicro")
	assertProfileNotSelected(t, match, "hgx-topology")
	assertProfileNotSelected(t, match, "msi")
	assertProfileNotSelected(t, match, "dell")

	if plan.Tuning.SnapshotMaxDocuments > 150000 {
		t.Fatalf("expected xfusion snapshot cap <= 150000, got %d", plan.Tuning.SnapshotMaxDocuments)
	}
	if plan.Tuning.PrefetchEnabled == nil || !*plan.Tuning.PrefetchEnabled {
		t.Fatal("expected xfusion fixture to enable prefetch")
	}
	if plan.Tuning.ETABaseline.SnapshotSeconds <= 0 {
		t.Fatal("expected xfusion snapshot eta baseline")
	}
	if !containsString(resolved.CriticalPaths, "/redfish/v1/Systems/1") {
		t.Fatal("expected system path in critical paths")
	}

	analysisPlan := ResolveAnalysisPlan(match, map[string]interface{}{
		"/redfish/v1/Systems/1/Processors/Gpu1": map[string]interface{}{"ProcessorType": "GPU"},
	}, DiscoveredResources{
		SystemPaths: []string{"/redfish/v1/Systems/1"},
	}, signals)
	if !analysisPlan.Directives.EnableProcessorGPUFallback {
		t.Fatal("expected xfusion analysis to enable processor GPU fallback when GPU processors present")
	}
	if !analysisPlan.Directives.EnableGenericGraphicsControllerDedup {
		t.Fatal("expected xfusion analysis to enable graphics controller dedup")
	}
}

func loadProfileFixtureSignals(t *testing.T, fixtureName string) MatchSignals {
	t.Helper()
	path := filepath.Join("testdata", fixtureName)
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read fixture %s: %v", path, err)
	}
	var signals MatchSignals
	if err := json.Unmarshal(data, &signals); err != nil {
		t.Fatalf("decode fixture %s: %v", path, err)
	}
	return normalizeSignals(signals)
}

func assertProfileSelected(t *testing.T, match MatchResult, want string) {
	t.Helper()
	for _, profile := range match.Profiles {
		if profile.Name() == want {
			return
		}
	}
	t.Fatalf("expected profile %q in %v", want, profileNames(match))
}

func assertProfileNotSelected(t *testing.T, match MatchResult, want string) {
	t.Helper()
	for _, profile := range match.Profiles {
		if profile.Name() == want {
			t.Fatalf("did not expect profile %q in %v", want, profileNames(match))
		}
	}
}

func profileNames(match MatchResult) []string {
	out := make([]string, 0, len(match.Profiles))
	for _, profile := range match.Profiles {
		out = append(out, profile.Name())
	}
	return out
}

func assertSameProfileNames(t *testing.T, left, right MatchResult) {
	t.Helper()
	leftNames := profileNames(left)
	rightNames := profileNames(right)
	if len(leftNames) != len(rightNames) {
		t.Fatalf("profile stack size differs: %v vs %v", leftNames, rightNames)
	}
	for i := range leftNames {
		if leftNames[i] != rightNames[i] {
			t.Fatalf("profile stack differs: %v vs %v", leftNames, rightNames)
		}
	}
}

func containsString(items []string, want string) bool {
	for _, item := range items {
		if item == want {
			return true
		}
	}
	return false
}

func discoveredResourcesFromSignals(signals MatchSignals) DiscoveredResources {
	var discovered DiscoveredResources
	for _, hint := range signals.ResourceHints {
		memberPath := discoveredMemberPath(hint)
		switch {
		case strings.HasPrefix(memberPath, "/redfish/v1/Systems/"):
			discovered.SystemPaths = append(discovered.SystemPaths, memberPath)
		case strings.HasPrefix(memberPath, "/redfish/v1/Chassis/"):
			discovered.ChassisPaths = append(discovered.ChassisPaths, memberPath)
		case strings.HasPrefix(memberPath, "/redfish/v1/Managers/"):
			discovered.ManagerPaths = append(discovered.ManagerPaths, memberPath)
		}
	}
	discovered.SystemPaths = dedupeSorted(discovered.SystemPaths)
	discovered.ChassisPaths = dedupeSorted(discovered.ChassisPaths)
	discovered.ManagerPaths = dedupeSorted(discovered.ManagerPaths)
	return discovered
}

func discoveredMemberPath(path string) string {
	path = strings.TrimSpace(path)
	if path == "" {
		return ""
	}
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) < 4 || parts[0] != "redfish" || parts[1] != "v1" {
		return ""
	}
	switch parts[2] {
	case "Systems", "Chassis", "Managers":
		return "/" + strings.Join(parts[:4], "/")
	default:
		return ""
	}
}
