package service

import (
	"context"
	"errors"
	"fmt"
	"path/filepath"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"oci-portal/internal/model"
)

// newLogEventEnv 建含回传所需表的内存库环境,并预置一个 OCI 配置。
func newLogEventEnv(t *testing.T) (*LogEventService, *gorm.DB, uint) {
	t.Helper()
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		t.Fatalf("open in-memory sqlite: %v", err)
	}
	// :memory: 库每个连接彼此独立,后台协程写库需复用同一连接
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("db handle: %v", err)
	}
	sqlDB.SetMaxOpenConns(1)
	if err := db.AutoMigrate(&model.Setting{}, &model.OciConfig{}, &model.LogEvent{}); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	cfg := model.OciConfig{Alias: "测试租户"}
	if err := db.Create(&cfg).Error; err != nil {
		t.Fatalf("create config: %v", err)
	}
	return NewLogEventService(db), db, cfg.ID
}

func TestEnsureAndResolveSecret(t *testing.T) {
	svc, db, cfgID := newLogEventEnv(t)
	ctx := context.Background()
	other := model.OciConfig{Alias: "另一租户"}
	if err := db.Create(&other).Error; err != nil {
		t.Fatalf("create config: %v", err)
	}

	first, err := svc.EnsureSecret(ctx, cfgID)
	if err != nil {
		t.Fatalf("ensure secret: %v", err)
	}
	if len(first.Secret) != 64 {
		t.Fatalf("secret length = %d, want 64", len(first.Secret))
	}
	if want := "/api/v1/webhooks/oci-logs/" + first.Secret; first.Path != want {
		t.Errorf("path = %s, want %s", first.Path, want)
	}

	again, err := svc.EnsureSecret(ctx, cfgID)
	if err != nil || again.Secret != first.Secret {
		t.Errorf("ensure not idempotent: %v, secret changed %v", err, again.Secret != first.Secret)
	}
	otherInfo, err := svc.EnsureSecret(ctx, other.ID)
	if err != nil {
		t.Fatalf("ensure other secret: %v", err)
	}

	tests := []struct {
		name    string
		secret  string
		wantID  uint
		wantHit bool
	}{
		{name: "命中本租户", secret: first.Secret, wantID: cfgID, wantHit: true},
		{name: "命中另一租户", secret: otherInfo.Secret, wantID: other.ID, wantHit: true},
		{name: "未知 secret 不命中", secret: "deadbeef", wantHit: false},
		{name: "空 secret 不命中", secret: "", wantHit: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			id, ok := svc.ResolveSecret(ctx, tt.secret)
			if ok != tt.wantHit || id != tt.wantID {
				t.Errorf("resolve = (%d,%v), want (%d,%v)", id, ok, tt.wantID, tt.wantHit)
			}
		})
	}

	if err := svc.RevokeSecret(ctx, cfgID); err != nil {
		t.Fatalf("revoke: %v", err)
	}
	if _, ok := svc.ResolveSecret(ctx, first.Secret); ok {
		t.Error("revoked secret still resolves")
	}
	if _, exists, _ := svc.SecretInfo(ctx, cfgID); exists {
		t.Error("secret info exists after revoke")
	}
}

func TestEnsureSecretRejectsUnknownConfig(t *testing.T) {
	svc, db, _ := newLogEventEnv(t)
	if _, err := svc.EnsureSecret(context.Background(), 9999); !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Fatalf("ensure unknown config error = %v, want record not found", err)
	}
	var count int64
	db.Model(&model.Setting{}).Where("key = ?", secretKey(9999)).Count(&count)
	if count != 0 {
		t.Fatalf("unknown config secret rows = %d, want 0", count)
	}
}

func TestEnsureSecretConcurrentDeleteNoOrphan(t *testing.T) {
	svc, db := newConcurrentSecretEnv(t)
	for i := 0; i < 8; i++ {
		cfg := model.OciConfig{Alias: fmt.Sprintf("race-%d", i)}
		if err := db.Create(&cfg).Error; err != nil {
			t.Fatalf("create config: %v", err)
		}
		ensureErr, deleteErr := runEnsureDeleteRace(svc, db, cfg.ID)
		if deleteErr != nil {
			t.Fatalf("delete config %d: %v", cfg.ID, deleteErr)
		}
		if ensureErr != nil && !errors.Is(ensureErr, gorm.ErrRecordNotFound) {
			t.Fatalf("ensure config %d: %v", cfg.ID, ensureErr)
		}
		assertNoSecretSetting(t, db, cfg.ID)
	}
}

func newConcurrentSecretEnv(t *testing.T) (*LogEventService, *gorm.DB) {
	t.Helper()
	dsn := filepath.Join(t.TempDir(), "secret.db") + "?_txlock=immediate&_pragma=busy_timeout%3d5000"
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("db handle: %v", err)
	}
	sqlDB.SetMaxOpenConns(4)
	t.Cleanup(func() { _ = sqlDB.Close() })
	if err := db.AutoMigrate(&model.Setting{}, &model.OciConfig{}); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	return NewLogEventService(db), db
}

func runEnsureDeleteRace(svc *LogEventService, db *gorm.DB, cfgID uint) (error, error) {
	start := make(chan struct{})
	ensureDone, deleteDone := make(chan error, 1), make(chan error, 1)
	go func() {
		<-start
		_, err := svc.EnsureSecret(context.Background(), cfgID)
		ensureDone <- err
	}()
	go func() {
		<-start
		deleteDone <- deleteConfigAndSecret(db, cfgID)
	}()
	close(start)
	return <-ensureDone, <-deleteDone
}

func deleteConfigAndSecret(db *gorm.DB, cfgID uint) error {
	return db.Transaction(func(tx *gorm.DB) error {
		if err := lockOciConfig(tx, cfgID); err != nil {
			return err
		}
		if err := tx.Delete(&model.Setting{}, "key = ?", secretKey(cfgID)).Error; err != nil {
			return err
		}
		return tx.Delete(&model.OciConfig{}, cfgID).Error
	})
}

func assertNoSecretSetting(t *testing.T, db *gorm.DB, cfgID uint) {
	t.Helper()
	var count int64
	db.Model(&model.Setting{}).Where("key = ?", secretKey(cfgID)).Count(&count)
	if count != 0 {
		t.Fatalf("orphan secret rows for config %d = %d", cfgID, count)
	}
}

func TestResolveSecretRejectsOrphanSetting(t *testing.T) {
	svc, db, _ := newLogEventEnv(t)
	orphan := model.Setting{Key: secretKey(9999), Value: "orphan-secret"}
	if err := db.Create(&orphan).Error; err != nil {
		t.Fatalf("create orphan secret: %v", err)
	}
	if id, ok := svc.ResolveSecret(context.Background(), orphan.Value); ok || id != 0 {
		t.Fatalf("resolve orphan secret = (%d,%v), want (0,false)", id, ok)
	}
	info, ok, err := svc.SecretInfo(context.Background(), 9999)
	if !errors.Is(err, gorm.ErrRecordNotFound) || ok || info.Secret != "" {
		t.Fatalf("orphan secret info = (%q,%v,%v), want empty,false,record not found", info.Secret, ok, err)
	}
}

func TestIngestIdempotent(t *testing.T) {
	svc, db, cfgID := newLogEventEnv(t)
	ctx := context.Background()
	for i := 0; i < 2; i++ {
		if err := svc.Ingest(ctx, cfgID, "msg-1", []byte(`{"eventType":"t"}`), false); err != nil {
			t.Fatalf("ingest #%d: %v", i+1, err)
		}
	}
	if err := svc.Ingest(ctx, cfgID, "msg-2", []byte("cut"), true); err != nil {
		t.Fatalf("ingest truncated: %v", err)
	}
	var total int64
	db.Model(&model.LogEvent{}).Count(&total)
	if total != 2 {
		t.Errorf("rows = %d, want 2(重复 messageId 不落新行)", total)
	}
	var truncated model.LogEvent
	db.First(&truncated, "message_id = ?", "msg-2")
	if !truncated.Truncated {
		t.Error("truncated flag not persisted")
	}
}

func TestIngestRejectsUnknownConfig(t *testing.T) {
	svc, db, _ := newLogEventEnv(t)
	err := svc.Ingest(context.Background(), 9999, "orphan", []byte(`{}`), false)
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Fatalf("ingest unknown config error = %v, want record not found", err)
	}
	var count int64
	db.Model(&model.LogEvent{}).Where("message_id = ?", "orphan").Count(&count)
	if count != 0 {
		t.Fatalf("orphan event rows = %d, want 0", count)
	}
}

func TestParseLogEvent(t *testing.T) {
	ts := "2026-07-07T08:00:00Z"
	tests := []struct {
		name        string
		payload     string
		wantType    string
		wantSource  string
		wantIP      string
		wantTime    bool
		wantActor   string
		wantOutcome string
		wantDetail  string
	}{
		{
			name:       "CloudEvents 单事件",
			payload:    `{"eventType":"com.oraclecloud.ComputeApi.TerminateInstance","source":"ComputeApi","eventTime":"` + ts + `"}`,
			wantType:   "com.oraclecloud.ComputeApi.TerminateInstance",
			wantSource: "ComputeApi",
			wantTime:   true,
		},
		{
			name:     "type 别名",
			payload:  `{"type":"custom.event"}`,
			wantType: "custom.event",
		},
		{
			name:       "外层缺失取 data 内层",
			payload:    `{"data":{"eventType":"inner.event","source":"Audit"}}`,
			wantType:   "inner.event",
			wantSource: "Audit",
		},
		{
			name:     "数组取首个",
			payload:  `[{"eventType":"batch.first"},{"eventType":"batch.second"}]`,
			wantType: "batch.first",
		},
		{name: "非 JSON 全空", payload: "plain text message"},
		{name: "时间非法只丢时间", payload: `{"eventType":"x","eventTime":"not-a-time"}`, wantType: "x"},
		{
			name:     "Audit 事件提取 data.identity.ipAddress",
			payload:  `{"eventType":"com.oraclecloud.limits.ListLimitValues","data":{"identity":{"ipAddress":"203.0.113.9"}}}`,
			wantType: "com.oraclecloud.limits.ListLimitValues",
			wantIP:   "203.0.113.9",
		},
		{
			name:     "identity 缺失 IP 为空",
			payload:  `{"eventType":"x","data":{"identity":{}}}`,
			wantType: "x",
		},
		{
			name: "Audit v2 提取操作者成败与策略描述",
			payload: `{"eventType":"com.oraclecloud.identityControlPlane.CreatePolicy","data":{
				"identity":{"principalName":"Alfonso Garcia","ipAddress":"129.159.43.9"},
				"message":"ociportal-logs-sch CreatePolicy succeeded",
				"stateChange":{"current":{"description":"允许 Connector 发布到 ONS"}}}}`,
			wantType: "com.oraclecloud.identityControlPlane.CreatePolicy", wantIP: "129.159.43.9",
			wantActor: "Alfonso Garcia", wantOutcome: "成功", wantDetail: "允许 Connector 发布到 ONS",
		},
		{
			name: "IDCS 登录失败提取用户 IP 与原因",
			payload: `{"eventType":"com.oraclecloud.IdentitySignOn.InteractiveLogin","data":{
				"additionalDetails":{"actorName":"oci","clientIp":"155.117.82.111",
				"auditEventMapValue":"{\"eventId\":\"sso.authentication.failure\",\"message\":\"Authentication failure : incorrect password.\"}"}}}`,
			wantType: "com.oraclecloud.IdentitySignOn.InteractiveLogin", wantIP: "155.117.82.111",
			wantActor: "oci", wantOutcome: "失败", wantDetail: "Authentication failure : incorrect password.",
		},
		{
			name:        "message failed 后缀判失败",
			payload:     `{"eventType":"x","data":{"message":"vm TerminateInstance failed"}}`,
			wantType:    "x",
			wantOutcome: "失败",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := parseLogEvent([]byte(tt.payload))
			if got.EventType != tt.wantType || got.Source != tt.wantSource || got.SourceIP != tt.wantIP {
				t.Errorf("parse = (%q,%q,%q), want (%q,%q,%q)",
					got.EventType, got.Source, got.SourceIP, tt.wantType, tt.wantSource, tt.wantIP)
			}
			if got.Actor != tt.wantActor || got.Outcome != tt.wantOutcome || got.Detail != tt.wantDetail {
				t.Errorf("推送字段 = (%q,%q,%q), want (%q,%q,%q)",
					got.Actor, got.Outcome, got.Detail, tt.wantActor, tt.wantOutcome, tt.wantDetail)
			}
			if (got.EventTime != nil) != tt.wantTime {
				t.Errorf("eventTime present = %v, want %v", got.EventTime != nil, tt.wantTime)
			}
		})
	}
}

func TestParseOnceMarksProcessed(t *testing.T) {
	svc, db, cfgID := newLogEventEnv(t)
	ctx := context.Background()
	if err := svc.Ingest(ctx, cfgID, "m-json", []byte(`{"eventType":"a.b","source":"S"}`), false); err != nil {
		t.Fatalf("ingest: %v", err)
	}
	if err := svc.Ingest(ctx, cfgID, "m-text", []byte("opaque"), false); err != nil {
		t.Fatalf("ingest: %v", err)
	}
	svc.parseOnce(ctx)
	var parsed, opaque model.LogEvent
	db.First(&parsed, "message_id = ?", "m-json")
	db.First(&opaque, "message_id = ?", "m-text")
	if !parsed.Processed || parsed.EventType != "a.b" || parsed.Source != "S" {
		t.Errorf("parsed = %+v, want processed with type/source", parsed)
	}
	if !opaque.Processed || opaque.EventType != "" {
		t.Errorf("opaque = %+v, want processed without fields", opaque)
	}
}

func TestUpdateParsedEventDoesNotRecreateDeleted(t *testing.T) {
	svc, db, cfgID := newLogEventEnv(t)
	event := model.LogEvent{OciConfigID: cfgID, MessageID: "deleted", Payload: `{}`}
	if err := db.Create(&event).Error; err != nil {
		t.Fatalf("create event: %v", err)
	}
	if err := db.Delete(&model.LogEvent{}, event.ID).Error; err != nil {
		t.Fatalf("delete event: %v", err)
	}
	if svc.updateParsedEvent(context.Background(), &event, parsedEvent{EventType: "x"}) {
		t.Fatal("deleted event update reported success")
	}
	var count int64
	db.Model(&model.LogEvent{}).Where("id = ?", event.ID).Count(&count)
	if count != 0 {
		t.Fatalf("deleted event was recreated, rows = %d", count)
	}
}

func TestUpdateParsedEventOnlyOnce(t *testing.T) {
	svc, db, cfgID := newLogEventEnv(t)
	event := model.LogEvent{OciConfigID: cfgID, MessageID: "once", Payload: `{}`}
	if err := db.Create(&event).Error; err != nil {
		t.Fatalf("create event: %v", err)
	}
	parsed := parsedEvent{EventType: "x"}
	if !svc.updateParsedEvent(context.Background(), &event, parsed) {
		t.Fatal("first conditional update should succeed")
	}
	if svc.updateParsedEvent(context.Background(), &event, parsed) {
		t.Fatal("processed event should not update twice")
	}
}

func TestLogEventCleanup(t *testing.T) {
	svc, db, cfgID := newLogEventEnv(t)
	ctx := context.Background()
	old := model.LogEvent{OciConfigID: cfgID, MessageID: "old", ReceivedAt: time.Now().Add(-100 * 24 * time.Hour)}
	db.Create(&old)
	for i := 0; i < 4; i++ {
		db.Create(&model.LogEvent{
			OciConfigID: cfgID, MessageID: fmt.Sprintf("new-%d", i), ReceivedAt: time.Now(),
		})
	}
	if err := svc.cleanup(ctx, logEventRetention, 2); err != nil {
		t.Fatalf("cleanup: %v", err)
	}
	var total int64
	db.Model(&model.LogEvent{}).Count(&total)
	if total != 2 {
		t.Errorf("rows after cleanup = %d, want 2(过期删除+超量删最旧)", total)
	}
	var gone model.LogEvent
	if err := db.First(&gone, "message_id = ?", "old").Error; err == nil {
		t.Error("expired row survived cleanup")
	}
}

func TestLogEventListFilters(t *testing.T) {
	svc, db, cfgID := newLogEventEnv(t)
	ctx := context.Background()
	other := model.OciConfig{Alias: "旁租户"}
	db.Create(&other)
	for i := 0; i < 3; i++ {
		db.Create(&model.LogEvent{OciConfigID: cfgID, MessageID: fmt.Sprintf("a-%d", i), ReceivedAt: time.Now()})
	}
	db.Create(&model.LogEvent{OciConfigID: other.ID, MessageID: "b-0", ReceivedAt: time.Now()})

	tests := []struct {
		name      string
		query     LogEventQuery
		wantTotal int64
		wantLen   int
		wantFirst string
	}{
		{name: "全部倒序", query: LogEventQuery{}, wantTotal: 4, wantLen: 4, wantFirst: "b-0"},
		{name: "按租户过滤", query: LogEventQuery{CfgID: cfgID}, wantTotal: 3, wantLen: 3, wantFirst: "a-2"},
		{name: "分页第二页", query: LogEventQuery{CfgID: cfgID, Page: 2, PageSize: 2}, wantTotal: 3, wantLen: 1, wantFirst: "a-0"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			items, total, err := svc.List(ctx, tt.query)
			if err != nil {
				t.Fatalf("list: %v", err)
			}
			if total != tt.wantTotal || len(items) != tt.wantLen {
				t.Fatalf("total=%d len=%d, want %d/%d", total, len(items), tt.wantTotal, tt.wantLen)
			}
			if items[0].MessageID != tt.wantFirst {
				t.Errorf("first = %s, want %s", items[0].MessageID, tt.wantFirst)
			}
		})
	}
}
