/*
 * Copyright (c) 2026 Huawei Technologies Co., Ltd.
 * openFuyao is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */

package analyzer

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

	"many-core-orchestrator/api/v1alpha1"

	olog "gopkg.openfuyao.cn/common-modules/ologger/log"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestDetectionRulesEvaluateAndCompute(t *testing.T) {
	rules := DefaultDetectionRules()
	metrics := NodeMetrics{
		LLCMissRate:     0.30,
		LLCOccupancy:    0.80,
		BioLatencyP99Ms: 60,
		PSIAvailable:    true,
		PSIIOAvg10:      25,
		PSIMemAvg10:     12,
		PSIMemFullAvg10: 1,
	}

	detections := rules.Evaluate(metrics)
	if len(detections) != 3 {
		t.Fatalf("detections=%v, want cache/io/memory", detections)
	}
	if got := rules.Compute(metrics); got <= 0 || got > 1 {
		t.Fatalf("interference level=%f, want within (0,1]", got)
	}
}

func TestDefaultRuleEngineAndWeightedCalculator(t *testing.T) {
	metrics := NodeMetrics{LLCMissRate: 0.3, LLCOccupancy: 0.8, PSIAvailable: true}
	if len((DefaultRuleEngine{}).Evaluate(metrics)) == 0 {
		t.Fatal("default rule engine should detect cache pollution")
	}
	if got := (WeightedCalculator{}).Compute(metrics); got <= 0 {
		t.Fatalf("weighted calculator=%f", got)
	}
}

func TestRuleConfigStoreEvaluateAndCompute(t *testing.T) {
	store := NewRuleConfigStore(Config{}, olog.With("test", "rules"))
	metrics := NodeMetrics{LLCMissRate: 0.3, LLCOccupancy: 0.8, PSIAvailable: true}
	if len(store.Evaluate(metrics)) == 0 {
		t.Fatal("store should evaluate using current rules")
	}
	if got := store.Compute(metrics); got <= 0 {
		t.Fatalf("store compute=%f", got)
	}
}

func TestDetectionRulesDegradedPath(t *testing.T) {
	rules := DefaultDetectionRules()
	metrics := NodeMetrics{
		LLCMissRate:     0.10,
		LLCOccupancy:    0.50,
		BioLatencyP99Ms: 35,
		PSIAvailable:    false,
		MemAvailRatio:   0.75,
		PageScanRate:    6400,
	}

	detections := rules.Evaluate(metrics)
	if len(detections) != 0 {
		t.Fatalf("detections=%v", detections)
	}
	got := rules.Compute(metrics)
	if got < 0.45 || got > 0.46 {
		t.Fatalf("degraded interference level=%f, want about 0.453", got)
	}
}

func TestDetectionRulesDegradedMemoryPressureThresholds(t *testing.T) {
	rules := DefaultDetectionRules()
	tests := []struct {
		name    string
		metrics NodeMetrics
	}{
		{
			name: "low memory available",
			metrics: NodeMetrics{
				PSIAvailable:  false,
				MemAvailRatio: 0.69,
				PageScanRate:  0,
			},
		},
		{
			name: "high page scan rate",
			metrics: NodeMetrics{
				PSIAvailable:  false,
				MemAvailRatio: 0.90,
				PageScanRate:  10000,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			detections := rules.Evaluate(tt.metrics)
			if len(detections) != 1 || detections[0].Type != "memory-pressure" {
				t.Fatalf("detections=%v, want memory-pressure", detections)
			}
		})
	}
}

func TestLoadDetectionRulesSpecAppliesDefaults(t *testing.T) {
	rules, err := LoadDetectionRulesSpec([]byte(`{"weights":{"cache":0.5},"thresholds":{"cacheMissRate":0.9}}`))
	if err != nil {
		t.Fatal(err)
	}
	if rules.Weights.Cache != 0.5 || rules.Weights.Memory != 0.30 || rules.Weights.IO != 0.40 {
		t.Fatalf("weights=%+v", rules.Weights)
	}
	if rules.Thresholds.CacheMissRate != 0.9 || rules.Thresholds.LLCOccupancy != 0.70 {
		t.Fatalf("thresholds=%+v", rules.Thresholds)
	}
	if _, err := LoadDetectionRulesSpec([]byte(`{bad-json`)); err == nil {
		t.Fatal("expected invalid json error")
	}
}

func TestLoadDetectionRulesFromYAML(t *testing.T) {
	path := filepath.Join(t.TempDir(), "rules.yaml")
	if err := os.WriteFile(path, []byte("weights:\n  cache: 0.6\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	rules, hash, err := LoadDetectionRules(path)
	if err != nil {
		t.Fatal(err)
	}
	if rules.Weights.Cache != 0.6 || hash == "" {
		t.Fatalf("rules=%+v hash=%q", rules.Weights, hash)
	}
	if _, _, err := LoadDetectionRules(filepath.Join(t.TempDir(), "missing.yaml")); err == nil {
		t.Fatal("expected missing file error")
	}
}

func TestRuleConfigStoreApplyTargetAndIgnoreNonTarget(t *testing.T) {
	store := NewRuleConfigStore(Config{
		DetectionRulesGroup:     "mco.many-core.io",
		DetectionRulesVersion:   "v1alpha1",
		DetectionRulesResource:  "detectionruleconfigs",
		DetectionRulesNamespace: "mco-system",
		DetectionRulesName:      "cluster",
	}, olog.With("test", "rules"))

	ignored := &v1alpha1.DetectionRuleConfig{ObjectMeta: metav1.ObjectMeta{Name: "other", Namespace: "mco-system"}}
	if err := store.ApplyDetectionRuleConfig("test", ignored); err != nil {
		t.Fatal(err)
	}
	if meta := store.Metadata(); meta.Hash != "" {
		t.Fatalf("ignored config should not set metadata: %+v", meta)
	}

	target := &v1alpha1.DetectionRuleConfig{
		ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "mco-system"},
		Spec: v1alpha1.DetectionRuleConfigSpec{
			Weights: v1alpha1.DetectionRuleWeights{Cache: 0.7},
		},
	}
	if err := store.ApplyDetectionRuleConfig("test", target); err != nil {
		t.Fatal(err)
	}
	if got := store.rules().Weights.Cache; got != 0.7 {
		t.Fatalf("cache weight=%f, want 0.7", got)
	}
	if meta := store.Metadata(); meta.Hash == "" || meta.LastLoadedTime.IsZero() {
		t.Fatalf("metadata not set: %+v", meta)
	}
	store.KeepLastValidAfterDelete()
	if got := store.rules().Weights.Cache; got != 0.7 {
		t.Fatalf("delete should keep last valid config, got %f", got)
	}
}