package service

import (
	"context"
	"errors"
	"strings"
	"sync"
	"testing"
	"time"

	"oci-portal/internal/crypto"
	"oci-portal/internal/model"
	"oci-portal/internal/oci"
)

// fakeRelayClient 是可编程的回传链路假客户端;内嵌接口,未覆写方法不被调用。
// fail 按步骤名注入错误;calls 记录动作序列供断言编排与回滚顺序。
type fakeRelayClient struct {
	oci.Client
	mu               sync.Mutex
	calls            []string
	fail             map[string]error
	subStates        []string // GetRelaySubscription 依次返回的状态,超出取末位
	subIdx           int
	endpoint         string // 最近一次订阅创建收到的 endpoint
	connectorTimeout bool   // 模拟 Connector 已建但轮询超时
	state            oci.RelayState
}

func (f *fakeRelayClient) step(name string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.calls = append(f.calls, name)
	return f.fail[name]
}

func (f *fakeRelayClient) EnsureRelayTopic(context.Context, oci.Credentials) (oci.RelayResource, error) {
	if err := f.step("topic"); err != nil {
		return oci.RelayResource{}, err
	}
	return oci.RelayResource{ID: "t1", State: "ACTIVE", Created: true}, nil
}

func (f *fakeRelayClient) EnsureRelaySubscription(_ context.Context, _ oci.Credentials, topicID, endpoint string) (oci.RelayResource, error) {
	f.mu.Lock()
	f.endpoint = endpoint
	f.mu.Unlock()
	if err := f.step("sub"); err != nil {
		return oci.RelayResource{}, err
	}
	return oci.RelayResource{ID: "s1", State: "PENDING", Created: true}, nil
}

func (f *fakeRelayClient) GetRelaySubscription(context.Context, oci.Credentials, string) (oci.RelayResource, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.calls = append(f.calls, "getsub")
	state := "ACTIVE"
	if len(f.subStates) > 0 {
		i := f.subIdx
		if i >= len(f.subStates) {
			i = len(f.subStates) - 1
		}
		state = f.subStates[i]
		f.subIdx++
	}
	return oci.RelayResource{ID: "s1", State: state}, nil
}

func (f *fakeRelayClient) EnsureRelayPolicy(_ context.Context, _ oci.Credentials, homeRegion string) (oci.RelayResource, error) {
	if err := f.step("policy:" + homeRegion); err != nil {
		return oci.RelayResource{}, err
	}
	return oci.RelayResource{ID: "p1", State: "ACTIVE", Created: true}, nil
}

func (f *fakeRelayClient) EnsureRelayConnector(context.Context, oci.Credentials, string, string) (oci.RelayResource, error) {
	if err := f.step("connector"); err != nil {
		return oci.RelayResource{}, err
	}
	if f.connectorTimeout {
		return oci.RelayResource{ID: "c1", State: "CREATING", Created: true}, errors.New("service connector not active after 90s")
	}
	return oci.RelayResource{ID: "c1", State: "ACTIVE", Created: true}, nil
}

func (f *fakeRelayClient) RelayState(context.Context, oci.Credentials, string) (oci.RelayState, error) {
	if err := f.step("state"); err != nil {
		return oci.RelayState{}, err
	}
	return f.state, nil
}

func (f *fakeRelayClient) DeleteRelayConnector(context.Context, oci.Credentials, string) error {
	return f.step("del-connector")
}

func (f *fakeRelayClient) DeleteRelayPolicy(context.Context, oci.Credentials, string, string) error {
	return f.step("del-policy")
}

func (f *fakeRelayClient) DeleteRelaySubscription(context.Context, oci.Credentials, string) error {
	return f.step("del-sub")
}

func (f *fakeRelayClient) DeleteRelayTopic(context.Context, oci.Credentials, string) error {
	return f.step("del-topic")
}

// deletes 过滤出删除动作序列,断言回滚/销毁顺序。
func (f *fakeRelayClient) deletes() []string {
	f.mu.Lock()
	defer f.mu.Unlock()
	var out []string
	for _, c := range f.calls {
		if strings.HasPrefix(c, "del-") {
			out = append(out, c)
		}
	}
	return out
}

// newRelayEnv 在 logevent 环境上补齐 relay 依赖:可解密凭据 + 假客户端 + 短轮询。
func newRelayEnv(t *testing.T, fc *fakeRelayClient) (*LogEventService, uint) {
	t.Helper()
	svc, db, cfgID := newLogEventEnv(t)
	cipher, err := crypto.NewCipher("test-key")
	if err != nil {
		t.Fatalf("new cipher: %v", err)
	}
	enc, err := cipher.EncryptString("-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----")
	if err != nil {
		t.Fatalf("encrypt key: %v", err)
	}
	var cfg model.OciConfig
	if err := db.First(&cfg, cfgID).Error; err != nil {
		t.Fatalf("load config: %v", err)
	}
	cfg.PrivateKeyEnc = enc
	cfg.TenancyOCID = "ocid1.tenancy.oc1..t"
	cfg.UserOCID = "ocid1.user.oc1..u"
	cfg.Region = "us-ashburn-1"
	cfg.Fingerprint = "aa:bb"
	cfg.HomeRegionKey = "IAD"
	if err := db.Save(&cfg).Error; err != nil {
		t.Fatalf("seed credentials: %v", err)
	}
	svc.SetRelayDeps(NewOciConfigService(db, cipher, fc), fc, "https://demo.example.com/")
	svc.relayPollTick = time.Millisecond
	svc.relayPollTimeout = 30 * time.Millisecond
	return svc, cfgID
}

func TestSetupRelayHappyPath(t *testing.T) {
	fc := &fakeRelayClient{state: oci.RelayState{
		Topic:        oci.RelayResource{ID: "t1", State: "ACTIVE"},
		Subscription: oci.RelayResource{ID: "s1", State: "ACTIVE"},
		Connector:    oci.RelayResource{ID: "c1", State: "ACTIVE"},
		Policy:       oci.RelayResource{ID: "p1", State: "ACTIVE"},
	}}
	svc, cfgID := newRelayEnv(t, fc)
	view, err := svc.SetupRelay(context.Background(), cfgID)
	if err != nil {
		t.Fatalf("SetupRelay: %v", err)
	}
	if !view.Ready {
		t.Errorf("Ready = false, want true; view = %+v", view)
	}
	if view.Webhook == nil || !strings.HasPrefix(view.Endpoint, "https://demo.example.com/api/v1/webhooks/oci-logs/") {
		t.Errorf("endpoint = %q, want 公网基址拼接", view.Endpoint)
	}
	if len(view.Events) != len(RelayCriticalEvents) {
		t.Errorf("events = %d 项, want %d", len(view.Events), len(RelayCriticalEvents))
	}
	if dels := fc.deletes(); len(dels) != 0 {
		t.Errorf("成功路径不应有删除调用,got %v", dels)
	}
	// 编排顺序:Topic → 订阅(endpoint 已拼接)→ 确认轮询 → Policy(home region)→ Connector
	joined := strings.Join(fc.calls, ",")
	for _, frag := range []string{"topic", "sub", "getsub", "policy:IAD", "connector"} {
		if !strings.Contains(joined, frag) {
			t.Errorf("calls = %s, 缺少 %s", joined, frag)
		}
	}
	if !strings.HasPrefix(fc.endpoint, "https://demo.example.com/api/v1/webhooks/oci-logs/") {
		t.Errorf("订阅 endpoint = %q, want 公网基址拼接", fc.endpoint)
	}
}

func TestSetupRelayRollback(t *testing.T) {
	tests := []struct {
		name       string
		program    func(fc *fakeRelayClient)
		wantErr    string
		wantDelSeq []string
	}{
		{
			name:       "订阅创建失败回滚 Topic",
			program:    func(fc *fakeRelayClient) { fc.fail = map[string]error{"sub": nil} },
			wantErr:    "创建订阅",
			wantDelSeq: []string{"del-topic"},
		},
		{
			name:       "Policy 失败回滚订阅与 Topic",
			program:    func(fc *fakeRelayClient) { fc.fail = map[string]error{"policy:IAD": errors.New("boom")} },
			wantErr:    "创建 Policy",
			wantDelSeq: []string{"del-sub", "del-topic"},
		},
		{
			name:       "Connector 创建失败回滚全部前置",
			program:    func(fc *fakeRelayClient) { fc.fail = map[string]error{"connector": errors.New("boom")} },
			wantErr:    "创建 Connector",
			wantDelSeq: []string{"del-policy", "del-sub", "del-topic"},
		},
		{
			name:       "Connector 超时时连自身一并回滚",
			program:    func(fc *fakeRelayClient) { fc.connectorTimeout = true },
			wantErr:    "创建 Connector",
			wantDelSeq: []string{"del-connector", "del-policy", "del-sub", "del-topic"},
		},
		{
			name:       "订阅确认超时回滚订阅与 Topic",
			program:    func(fc *fakeRelayClient) { fc.subStates = []string{"PENDING"} },
			wantErr:    "订阅确认超时",
			wantDelSeq: []string{"del-sub", "del-topic"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fc := &fakeRelayClient{}
			tt.program(fc)
			// map 值为 nil 的注入替换为真实错误(subtest 里统一)
			for k, v := range fc.fail {
				if v == nil {
					fc.fail[k] = errors.New("boom")
				}
			}
			svc, cfgID := newRelayEnv(t, fc)
			_, err := svc.SetupRelay(context.Background(), cfgID)
			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
				t.Fatalf("err = %v, want 含 %q", err, tt.wantErr)
			}
			dels := fc.deletes()
			if strings.Join(dels, ",") != strings.Join(tt.wantDelSeq, ",") {
				t.Errorf("回滚序列 = %v, want %v", dels, tt.wantDelSeq)
			}
		})
	}
}

func TestSetupRelayRequiresPublicURL(t *testing.T) {
	svc, _, cfgID := newLogEventEnv(t)
	if _, err := svc.SetupRelay(context.Background(), cfgID); !errors.Is(err, ErrRelayNotConfigured) {
		t.Errorf("err = %v, want ErrRelayNotConfigured", err)
	}
}

func TestTeardownRelay(t *testing.T) {
	fc := &fakeRelayClient{state: oci.RelayState{
		Topic:        oci.RelayResource{ID: "t1", State: "ACTIVE"},
		Subscription: oci.RelayResource{ID: "s1", State: "ACTIVE"},
		Connector:    oci.RelayResource{ID: "c1", State: "ACTIVE"},
		Policy:       oci.RelayResource{ID: "p1", State: "ACTIVE"},
	}}
	svc, cfgID := newRelayEnv(t, fc)
	ctx := context.Background()
	if _, err := svc.EnsureSecret(ctx, cfgID); err != nil {
		t.Fatalf("ensure secret: %v", err)
	}
	if err := svc.TeardownRelay(ctx, cfgID); err != nil {
		t.Fatalf("TeardownRelay: %v", err)
	}
	want := []string{"del-connector", "del-policy", "del-sub", "del-topic"}
	if dels := fc.deletes(); strings.Join(dels, ",") != strings.Join(want, ",") {
		t.Errorf("销毁序列 = %v, want %v", dels, want)
	}
	if _, exists, err := svc.SecretInfo(ctx, cfgID); err != nil || exists {
		t.Errorf("teardown 后 secret 仍存在 (exists=%v, err=%v)", exists, err)
	}
}

func TestRelayReady(t *testing.T) {
	active := func() oci.RelayState {
		return oci.RelayState{
			Topic:        oci.RelayResource{ID: "t", State: "ACTIVE"},
			Subscription: oci.RelayResource{ID: "s", State: "ACTIVE"},
			Connector:    oci.RelayResource{ID: "c", State: "ACTIVE"},
			Policy:       oci.RelayResource{ID: "p", State: "ACTIVE"},
		}
	}
	tests := []struct {
		name   string
		secret bool
		mutate func(*oci.RelayState)
		want   bool
	}{
		{name: "全链路活跃", secret: true, mutate: func(*oci.RelayState) {}, want: true},
		{name: "secret 未生成", secret: false, mutate: func(*oci.RelayState) {}, want: false},
		{name: "订阅 PENDING", secret: true, mutate: func(s *oci.RelayState) { s.Subscription.State = "PENDING" }, want: false},
		{name: "Connector 缺失", secret: true, mutate: func(s *oci.RelayState) { s.Connector = oci.RelayResource{} }, want: false},
		{name: "Policy 缺失", secret: true, mutate: func(s *oci.RelayState) { s.Policy = oci.RelayResource{} }, want: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			st := active()
			tt.mutate(&st)
			if got := relayReady(tt.secret, st); got != tt.want {
				t.Errorf("relayReady = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestCriticalEventText(t *testing.T) {
	tests := []struct {
		name   string
		event  parsedEvent
		wantOK bool
		want   map[string]string
	}{
		{
			name: "实例终止含操作者与成败",
			event: parsedEvent{EventType: "com.oraclecloud.ComputeApi.TerminateInstance",
				ResourceName: "vm-1", Actor: "demo@example.com", SourceIP: "203.0.113.8", Outcome: "成功"},
			wantOK: true,
			want: map[string]string{"event": "TerminateInstance", "resource": "vm-1",
				"actor": "demo@example.com", "ip": "203.0.113.8", "outcome": "成功", "detail": ""},
		},
		{
			name:   "字段缺失兜底破折号",
			event:  parsedEvent{EventType: "com.oraclecloud.IdentityControlPlane.CreateApiKey"},
			wantOK: true,
			want: map[string]string{"event": "CreateApiKey", "resource": "—",
				"actor": "—", "ip": "—", "outcome": "—", "detail": ""},
		},
		{
			name:   "补充说明独立成行",
			event:  parsedEvent{EventType: "CreatePolicy", Detail: "允许发布到 ONS Topic"},
			wantOK: true,
			want:   map[string]string{"event": "CreatePolicy", "detail": "\n允许发布到 ONS Topic"},
		},
		{name: "List 噪声不推", event: parsedEvent{EventType: "com.oraclecloud.ComputeApi.ListInstances"}, wantOK: false},
		{name: "空类型不推", event: parsedEvent{}, wantOK: false},
		{name: "短名直接命中", event: parsedEvent{EventType: "LaunchInstance"}, wantOK: true,
			want: map[string]string{"event": "LaunchInstance"}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			vars, ok := criticalEventVars("免费01", tt.event)
			if ok != tt.wantOK {
				t.Fatalf("ok = %v, want %v", ok, tt.wantOK)
			}
			if !ok {
				return
			}
			if vars["tenant"] != "免费01" {
				t.Errorf("tenant = %q, want 免费01", vars["tenant"])
			}
			for k, want := range tt.want {
				if vars[k] != want {
					t.Errorf("%s = %q, want %q", k, vars[k], want)
				}
			}
		})
	}
}

func TestRelayEventClass(t *testing.T) {
	cases := []struct {
		name  string
		class string
	}{
		{"LaunchInstance", "instance"},
		{"TerminateInstance", "instance"},
		{"InstanceAction", "instance"},
		{"CreateUser", "identity"},
		{"DeleteApiKey", "identity"},
		{"UpdateUserCapabilities", "identity"},
		{"CreatePolicy", "policy"},
		{"DeletePolicy", "policy"},
		{"CreateRegionSubscription", "region"},
		{"InteractiveLogin", "login"},
		{"FederatedInteractiveLogin", "login"},
		{"ListInstances", ""},
	}
	for _, tc := range cases {
		if got := relayEventClass(tc.name); got != tc.class {
			t.Errorf("relayEventClass(%s) = %q, want %q", tc.name, got, tc.class)
		}
	}
	// 关键事件清单里的每个事件都必须有归属子类,防新增事件漏配开关
	for _, e := range RelayCriticalEvents {
		if relayEventClass(e) == "" {
			t.Errorf("critical event %s has no notify class", e)
		}
	}
}
