package service

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"oci-portal/internal/oci"
)

// auditStubClient 覆写审计查询并记录透传参数，其余行为沿用 fakeClient。
type auditStubClient struct {
	*fakeClient
	batch     oci.AuditBatchResult
	result    oci.AuditEventsResult
	gotRegion string
	gotCursor oci.AuditCursor
	gotLimit  int
	gotStart  time.Time
	gotEnd    time.Time
	calls     int
}

func (f *auditStubClient) ListAuditEventsBatch(ctx context.Context, cred oci.Credentials, region string, cur oci.AuditCursor, limit int) (oci.AuditBatchResult, error) {
	f.calls++
	f.gotRegion, f.gotCursor, f.gotLimit = region, cur, limit
	return f.batch, nil
}

func (f *auditStubClient) ListAuditEvents(ctx context.Context, cred oci.Credentials, region string, start, end time.Time, page string) (oci.AuditEventsResult, error) {
	f.calls++
	f.gotRegion, f.gotStart, f.gotEnd = region, start, end
	return f.result, nil
}

func TestAuditCursorCodec(t *testing.T) {
	cur := oci.AuditCursor{
		Start:       time.Date(2026, 7, 9, 10, 0, 0, 0, time.UTC),
		End:         time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC),
		Page:        "tok-1",
		WindowHours: 48,
	}
	got, err := decodeAuditCursor(encodeAuditCursor(cur))
	if err != nil {
		t.Fatalf("roundtrip: %v", err)
	}
	if !got.Start.Equal(cur.Start) || !got.End.Equal(cur.End) || got.Page != cur.Page || got.WindowHours != cur.WindowHours {
		t.Fatalf("roundtrip = %+v, want %+v", got, cur)
	}

	// 空串 → 自当前时刻首查:24h 窗、分钟粒度、无窗内游标
	first, err := decodeAuditCursor("")
	if err != nil {
		t.Fatalf("first cursor: %v", err)
	}
	if first.End.Sub(first.Start) != 24*time.Hour || first.Page != "" || first.End.Second() != 0 {
		t.Fatalf("first cursor = %+v, 应为 24h 分钟粒度首窗", first)
	}

	bad := []string{"!!!", "bm90LWpzb24", encodeAuditCursor(oci.AuditCursor{})}
	for i, s := range bad {
		if _, err := decodeAuditCursor(s); !errors.Is(err, ErrInvalidAuditCursor) {
			t.Errorf("bad[%d] err = %v, want ErrInvalidAuditCursor", i, err)
		}
	}
}

func TestAuditEventsBatchParams(t *testing.T) {
	next := oci.AuditCursor{
		Start:       time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC),
		End:         time.Date(2026, 7, 9, 10, 0, 0, 0, time.UTC),
		WindowHours: 24,
	}
	client := &auditStubClient{
		fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
		batch: oci.AuditBatchResult{
			Items:  []oci.AuditEvent{{EventId: "e1", EventName: "GetInstance"}},
			Cursor: &next,
		},
	}
	svc := newTestService(t, client)
	cfg := importAliveConfig(t, svc)
	ctx := context.Background()

	// limit 归一:0 → 100;超上限截到 200;region 透传
	cases := []struct {
		name      string
		limit     int
		wantLimit int
	}{
		{"缺省 100", 0, 100},
		{"正常透传", 50, 50},
		{"超限截断", 999, 200},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{Region: "ap-tokyo-1", Limit: tc.limit})
			if err != nil {
				t.Fatalf("AuditEvents: %v", err)
			}
			if client.gotLimit != tc.wantLimit || client.gotRegion != "ap-tokyo-1" {
				t.Fatalf("limit = %d(want %d), region = %q", client.gotLimit, tc.wantLimit, client.gotRegion)
			}
			if got.Cursor == "" || got.Exhausted {
				t.Fatalf("响应应携带续查游标: %+v", got)
			}
		})
	}

	// 续查:响应游标原样带回可解析,并透传到 oci 层
	got, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{})
	if err != nil {
		t.Fatalf("首查: %v", err)
	}
	if _, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{Cursor: got.Cursor}); err != nil {
		t.Fatalf("续查: %v", err)
	}
	if !client.gotCursor.Start.Equal(next.Start) || client.gotCursor.WindowHours != 24 {
		t.Fatalf("续查游标透传 = %+v, want %+v", client.gotCursor, next)
	}

	// 尽头:Cursor 为 nil → 响应空游标 + exhausted
	client.batch = oci.AuditBatchResult{Items: []oci.AuditEvent{}, Exhausted: true}
	end, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{})
	if err != nil || end.Cursor != "" || !end.Exhausted {
		t.Fatalf("尽头响应 = %+v, %v", end, err)
	}

	// 非法游标 → ErrInvalidAuditCursor
	if _, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{Cursor: "!!!"}); !errors.Is(err, ErrInvalidAuditCursor) {
		t.Fatalf("非法游标 err = %v", err)
	}
}

func TestAuditRawStrippedAndDetail(t *testing.T) {
	raw := json.RawMessage(`{"eventId":"evt-1","full":true}`)
	eventTime := time.Date(2026, 7, 9, 10, 30, 40, 0, time.UTC)
	client := &auditStubClient{
		fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
		batch: oci.AuditBatchResult{Items: []oci.AuditEvent{
			{EventId: "evt-1", EventTime: &eventTime, EventName: "TerminateInstance", Raw: raw},
		}},
	}
	svc := newTestService(t, client)
	cfg := importAliveConfig(t, svc)
	ctx := context.Background()

	got, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{})
	if err != nil {
		t.Fatalf("AuditEvents: %v", err)
	}
	if got.Items[0].Raw != nil {
		t.Error("列表响应应剥离 raw")
	}
	// 详情走缓存,不再回源
	detail, err := svc.AuditEventDetail(ctx, cfg.ID, "", "evt-1", eventTime)
	if err != nil || string(detail) != string(raw) {
		t.Fatalf("AuditEventDetail = %s, %v", detail, err)
	}
	if client.calls != 1 {
		t.Errorf("缓存命中不应回源, calls = %d", client.calls)
	}
}

func TestAuditDetailRequeryFallback(t *testing.T) {
	raw := json.RawMessage(`{"eventId":"evt-2"}`)
	eventTime := time.Date(2026, 7, 9, 10, 30, 40, 0, time.UTC)
	client := &auditStubClient{
		fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
		result: oci.AuditEventsResult{Items: []oci.AuditEvent{
			{EventId: "evt-2", EventTime: &eventTime, Raw: raw},
		}},
	}
	svc := newTestService(t, client)
	cfg := importAliveConfig(t, svc)
	ctx := context.Background()

	// 缓存未预热:miss 走事件时间小窗重查找回
	detail, err := svc.AuditEventDetail(ctx, cfg.ID, "", "evt-2", eventTime)
	if err != nil || string(detail) != string(raw) {
		t.Fatalf("兜底重查 = %s, %v", detail, err)
	}
	if client.calls != 1 {
		t.Fatalf("应回源一次, calls = %d", client.calls)
	}
	if win := client.gotEnd.Sub(client.gotStart); win != 2*time.Minute || client.gotStart.Second() != 0 {
		t.Errorf("兜底窗口 = [%v, +%v), 应为事件分钟起 2 分钟", client.gotStart, win)
	}
	// 重查也找不到 → ErrAuditEventGone
	if _, err := svc.AuditEventDetail(ctx, cfg.ID, "", "no-such", eventTime); !errors.Is(err, ErrAuditEventGone) {
		t.Errorf("未找回 err = %v, want ErrAuditEventGone", err)
	}
}

func TestAuditRawCacheTenantIsolationAndInvalidation(t *testing.T) {
	svc := newTestService(t, &fakeClient{})
	raw1 := json.RawMessage(`{"tenant":1}`)
	raw2 := json.RawMessage(`{"tenant":2}`)
	svc.stripAuditRaw(1, []oci.AuditEvent{{EventId: "same", Raw: raw1}})
	svc.stripAuditRaw(2, []oci.AuditEvent{{EventId: "same", Raw: raw2}})

	assertAuditRaw(t, svc, 1, raw1)
	assertAuditRaw(t, svc, 2, raw2)
	svc.InvalidateAuditCache(1)
	if _, ok := svc.auditRaw.Get(auditRawKey(1, "same")); ok {
		t.Fatal("tenant 1 raw cache still exists after invalidation")
	}
	assertAuditRaw(t, svc, 2, raw2)
}

func assertAuditRaw(t *testing.T, svc *OciConfigService, configID uint, want json.RawMessage) {
	t.Helper()
	got, ok := svc.auditRaw.Get(auditRawKey(configID, "same"))
	if !ok || string(got.(json.RawMessage)) != string(want) {
		t.Fatalf("tenant %d raw = %v, want %s", configID, got, want)
	}
}
