package oci

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/oracle/oci-go-sdk/v65/audit"
	"github.com/oracle/oci-go-sdk/v65/common"
	"github.com/oracle/oci-go-sdk/v65/loggingsearch"
)

// quotaZeroErr 复刻 Search 配额为零租户的真实报错(SDK 解析错误体失败后带原文)。
var quotaZeroErr = errors.New(`search audit logs: Failed to parse json from response body due to: json: cannot unmarshal number into Go struct field servicefailure.code of type string. With response body { "code" : 500, "message" : "Rate limit exceeded for ocid: ocid1.tenancy..x, maxQueriesPerMinute: 0, maxConcurrentQueries: 0" }.`)

func TestIsSearchQuotaZero(t *testing.T) {
	cases := []struct {
		name string
		err  error
		want bool
	}{
		{"配额为零真实报错", quotaZeroErr, true},
		{"普通限流不回退", errors.New(`Rate limit exceeded for ocid: x, maxQueriesPerMinute: 60, maxConcurrentQueries: 2`), false},
		{"其他错误", errors.New("service unavailable"), false},
		{"nil", nil, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := isSearchQuotaZero(tc.err); got != tc.want {
				t.Fatalf("isSearchQuotaZero() = %v, want %v", got, tc.want)
			}
		})
	}
}

func TestListAuditBatchFallback(t *testing.T) {
	et := time.Now().UTC().Add(-10 * time.Minute)
	searchCalls, auditCalls := 0, 0
	f := auditFetchers{
		search: func(context.Context, AuditCursor) ([]AuditEvent, string, error) {
			searchCalls++
			return nil, "", quotaZeroErr
		},
		audit: func(_ context.Context, cur AuditCursor) ([]AuditEvent, string, error) {
			auditCalls++
			if cur.M != auditModeFallback || cur.Page != "" {
				t.Fatalf("回退通道应携带模式标记且清空页游标, got %+v", cur)
			}
			ev := AuditEvent{EventId: fmt.Sprint(auditCalls), EventName: "GetInstance", EventTime: &et}
			return []AuditEvent{ev}, "", nil
		},
	}
	res, err := listAuditBatch(context.Background(), f, NewAuditCursor(time.Now()), 3)
	if err != nil {
		t.Fatalf("配额为零应回退成功, got %v", err)
	}
	if searchCalls != 1 {
		t.Fatalf("Search 只应试错一次, got %d", searchCalls)
	}
	if len(res.Items) < 3 || auditCalls < 3 {
		t.Fatalf("回退后应继续凑批, items=%d auditCalls=%d", len(res.Items), auditCalls)
	}
	if res.Cursor == nil || res.Cursor.M != auditModeFallback || res.Cursor.WindowHours != auditFallbackWindowHours {
		t.Fatalf("续查游标应保持回退模式与基准窗宽, got %+v", res.Cursor)
	}
}

func TestListAuditBatchFallbackCursorSkipsSearch(t *testing.T) {
	et := time.Now().UTC().Add(-10 * time.Minute)
	f := auditFetchers{
		search: func(context.Context, AuditCursor) ([]AuditEvent, string, error) {
			t.Fatal("回退模式游标不应再调用 Search 通道")
			return nil, "", nil
		},
		audit: func(context.Context, AuditCursor) ([]AuditEvent, string, error) {
			return []AuditEvent{{EventId: "e1", EventName: "GetVcn", EventTime: &et}}, "", nil
		},
	}
	cur := NewAuditCursor(time.Now()).toFallback()
	if _, err := listAuditBatch(context.Background(), f, cur, 1); err != nil {
		t.Fatalf("回退模式续查失败: %v", err)
	}
}

func TestListAuditBatchSearchErrorNoFallback(t *testing.T) {
	f := auditFetchers{
		search: func(context.Context, AuditCursor) ([]AuditEvent, string, error) {
			return nil, "", errors.New("search audit logs: timeout")
		},
		audit: func(context.Context, AuditCursor) ([]AuditEvent, string, error) {
			t.Fatal("普通错误不应触发回退")
			return nil, "", nil
		},
	}
	if _, err := listAuditBatch(context.Background(), f, NewAuditCursor(time.Now()), 1); err == nil {
		t.Fatal("普通错误应原样上抛")
	}
}

func TestFilterAuditTerm(t *testing.T) {
	login := AuditEvent{EventId: "e1", EventName: "InteractiveLogin"}
	noise := AuditEvent{EventId: "e2", EventName: "ListRecommendations"}
	items := []AuditEvent{login, noise}
	cases := []struct {
		name string
		cur  AuditCursor
		want int
	}{
		{"无关键字原样放行", AuditCursor{}, 2},
		{"Search 主路也精筛可见字段", AuditCursor{Q: "login"}, 1},
		{"回退模式精筛", AuditCursor{Q: "login", M: auditModeFallback}, 1},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := filterAuditTerm(append([]AuditEvent{}, items...), tc.cur); len(got) != tc.want {
				t.Fatalf("filterAuditTerm() 保留 %d 条, want %d", len(got), tc.want)
			}
		})
	}
}

func TestListAuditBatchSearchTermPrecision(t *testing.T) {
	et := time.Now().UTC().Add(-10 * time.Minute)
	// 模拟 Search 主路粗筛后仍混入的隐藏元数据误命中(如 ttype:login)
	f := auditFetchers{
		search: func(_ context.Context, cur AuditCursor) ([]AuditEvent, string, error) {
			return []AuditEvent{
				{EventId: "hit", EventName: "InteractiveLogin", EventTime: &et},
				{EventId: "noise1", EventName: "ListRecommendations", EventTime: &et},
				{EventId: "noise2", EventName: "SearchLogs", EventTime: &et},
			}, "", nil
		},
		audit: func(context.Context, AuditCursor) ([]AuditEvent, string, error) {
			t.Fatal("Search 正常时不应走回退")
			return nil, "", nil
		},
	}
	cur := NewAuditCursor(time.Now())
	cur.Q = "login"
	res, err := listAuditBatch(context.Background(), f, cur, 1)
	if err != nil {
		t.Fatalf("listAuditBatch() err = %v", err)
	}
	if len(res.Items) != 1 || res.Items[0].EventId != "hit" {
		t.Fatalf("应只保留可见字段命中的事件, got %+v", res.Items)
	}
}

func TestMatchesAuditTerm(t *testing.T) {
	ev := AuditEvent{EventName: "ListVnicAttachments", ResourceName: "web-1", PrincipalName: "Vivien", IPAddress: "1.2.3.4"}
	cases := []struct {
		name string
		q    string
		want bool
	}{
		{"不区分大小写", "listvnic", true},
		{"通配分段都出现", "List*Attachments", true},
		{"资源名命中", "WEB-1", true},
		{"未命中", "TerminateInstance", false},
		{"通配缺段不命中", "List*Volume", false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := matchesAuditTerm(ev, tc.q); got != tc.want {
				t.Fatalf("matchesAuditTerm(%q) = %v, want %v", tc.q, got, tc.want)
			}
		})
	}
}

// searchResultFromJSON 把 JSON 文本构造成 SearchLogs 单条结果(Data 为 interface{})。
func searchResultFromJSON(t *testing.T, s string) loggingsearch.SearchResult {
	t.Helper()
	var v interface{}
	if err := json.Unmarshal([]byte(s), &v); err != nil {
		t.Fatalf("fixture 不是合法 JSON: %v", err)
	}
	return loggingsearch.SearchResult{Data: &v}
}

func TestToSearchAuditEvent(t *testing.T) {
	eventTime := time.Date(2026, 7, 6, 10, 30, 0, 0, time.UTC)
	tests := []struct {
		name   string
		data   string
		wantOK bool
		want   AuditEvent
	}{
		{
			name: "全字段齐全",
			data: `{"datetime":1783074600000,"logContent":{
				"id":"evt-abc","time":"2026-07-06T10:30:00Z","source":"ComputeApi",
				"data":{"eventName":"TerminateInstance","resourceName":"web-1","compartmentName":"prod",
					"identity":{"principalName":"api-admin","ipAddress":"1.2.3.4"},
					"request":{"action":"DELETE","path":"/20160918/instances/ocid1..."},
					"response":{"status":"204"}}}}`,
			wantOK: true,
			want: AuditEvent{
				EventId:         "evt-abc",
				EventTime:       &eventTime,
				EventName:       "TerminateInstance",
				Source:          "ComputeApi",
				ResourceName:    "web-1",
				CompartmentName: "prod",
				PrincipalName:   "api-admin",
				IPAddress:       "1.2.3.4",
				Status:          "204",
				RequestAction:   "DELETE",
				RequestPath:     "/20160918/instances/ocid1...",
			},
		},
		{
			name: "identity/request/response 为 null 时只保留信封字段",
			data: `{"logContent":{"id":"evt-x","time":"2026-07-06T10:30:00Z","source":"VcnApi",
				"data":{"eventName":"GetVcn","identity":null,"request":null,"response":null}}}`,
			wantOK: true,
			want:   AuditEvent{EventId: "evt-x", EventTime: &eventTime, Source: "VcnApi", EventName: "GetVcn"},
		},
		{
			name:   "缺 logContent 丢弃",
			data:   `{"datetime":1783074600000}`,
			wantOK: false,
		},
		{
			name:   "logContent 结构不符丢弃",
			data:   `{"logContent":"plain-text"}`,
			wantOK: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, ok := toSearchAuditEvent(searchResultFromJSON(t, tt.data))
			if ok != tt.wantOK {
				t.Fatalf("ok = %v, want %v", ok, tt.wantOK)
			}
			if !ok {
				return
			}
			if len(got.Raw) == 0 {
				t.Fatalf("Raw 应携带 logContent 原文")
			}
			if !auditEventEqual(got, tt.want) {
				t.Errorf("toSearchAuditEvent() = %+v, want %+v", got, tt.want)
			}
		})
	}
}

func TestToSearchAuditEventNilData(t *testing.T) {
	if _, ok := toSearchAuditEvent(loggingsearch.SearchResult{}); ok {
		t.Fatal("Data 为 nil 应丢弃")
	}
}

func TestToAuditEvent(t *testing.T) {
	eventTime := time.Date(2026, 7, 6, 10, 30, 0, 0, time.UTC)
	tests := []struct {
		name string
		ev   audit.AuditEvent
		want AuditEvent
	}{
		{
			name: "全字段齐全",
			ev: audit.AuditEvent{
				EventId:   common.String("evt-abc"),
				Source:    common.String("ComputeApi"),
				EventTime: &common.SDKTime{Time: eventTime},
				Data: &audit.Data{
					EventName:       common.String("TerminateInstance"),
					ResourceName:    common.String("web-1"),
					CompartmentName: common.String("prod"),
					Identity: &audit.Identity{
						PrincipalName: common.String("api-admin"),
						IpAddress:     common.String("1.2.3.4"),
					},
					Request: &audit.Request{
						Action: common.String("DELETE"),
						Path:   common.String("/20160918/instances/ocid1..."),
					},
					Response: &audit.Response{Status: common.String("204")},
				},
			},
			want: AuditEvent{
				EventId: "evt-abc", EventTime: &eventTime, EventName: "TerminateInstance",
				Source: "ComputeApi", ResourceName: "web-1", CompartmentName: "prod",
				PrincipalName: "api-admin", IPAddress: "1.2.3.4", Status: "204",
				RequestAction: "DELETE", RequestPath: "/20160918/instances/ocid1...",
			},
		},
		{
			name: "嵌套局部 nil 各自安全跳过",
			ev: audit.AuditEvent{
				Data: &audit.Data{
					EventName: common.String("GetInstance"),
					Request:   &audit.Request{Path: common.String("/instances")},
				},
			},
			want: AuditEvent{EventName: "GetInstance", RequestPath: "/instances"},
		},
		{name: "空事件全部零值", ev: audit.AuditEvent{}, want: AuditEvent{}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := toAuditEvent(tt.ev); !auditEventEqual(got, tt.want) {
				t.Errorf("toAuditEvent() = %+v, want %+v", got, tt.want)
			}
		})
	}
}

func TestAuditSearchQuery(t *testing.T) {
	const prefix = `search "ocid1.tenancy.oc1..aaa/_Audit" | where data.eventName != 'SummarizeMetricsData'`
	cases := []struct {
		name string
		term string
		want string
	}{
		{"无关键字", "", prefix + ` | sort by datetime desc`},
		{"带关键字追加全文匹配", "TerminateInstance", prefix + ` and logContent = '*TerminateInstance*' | sort by datetime desc`},
		{"引号与反斜杠被消毒", `O'Brien\"x`, prefix + ` and logContent = '*OBrienx*' | sort by datetime desc`},
		{"纯引号消毒后为空不追加", `'"`, prefix + ` | sort by datetime desc`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := auditSearchQuery("ocid1.tenancy.oc1..aaa", tc.term); got != tc.want {
				t.Fatalf("auditSearchQuery() = %q, want %q", got, tc.want)
			}
		})
	}
}

func TestSanitizeAuditTerm(t *testing.T) {
	if got := SanitizeAuditTerm("  Get*Instance\t "); got != "Get*Instance" {
		t.Fatalf("应保留 * 并去除首尾空白与控制字符, got %q", got)
	}
	long := strings.Repeat("a", 300)
	if got := SanitizeAuditTerm(long); len(got) != auditTermMaxLen {
		t.Fatalf("超长应截断到 %d, got %d", auditTermMaxLen, len(got))
	}
}

// auditEventEqual 比较两个 DTO：EventTime 按值比较,Raw 不参与,其余反射比较。
func auditEventEqual(a, b AuditEvent) bool {
	if (a.EventTime == nil) != (b.EventTime == nil) {
		return false
	}
	if a.EventTime != nil && !a.EventTime.Equal(*b.EventTime) {
		return false
	}
	a.EventTime, b.EventTime = nil, nil
	a.Raw, b.Raw = nil, nil
	return reflect.DeepEqual(a, b)
}

func TestSortAuditEvents(t *testing.T) {
	t1 := time.Date(2026, 7, 6, 8, 0, 0, 0, time.UTC)
	t2 := time.Date(2026, 7, 6, 9, 0, 0, 0, time.UTC)
	items := []AuditEvent{
		{EventName: "old", EventTime: &t1},
		{EventName: "no-time", EventTime: nil},
		{EventName: "new", EventTime: &t2},
	}
	sortAuditEvents(items)
	got := []string{items[0].EventName, items[1].EventName, items[2].EventName}
	want := []string{"new", "old", "no-time"}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("sortAuditEvents() order = %v, want %v", got, want)
		}
	}
}

func TestKeepAuditEvent(t *testing.T) {
	cases := []struct {
		name  string
		event AuditEvent
		keep  bool
	}{
		{"公网IP的普通事件保留", AuditEvent{EventName: "CreateUser", IPAddress: "203.0.113.7"}, true},
		{"SummarizeMetricsData 噪声排除", AuditEvent{EventName: "SummarizeMetricsData", IPAddress: "203.0.113.7"}, false},
		{"内网10段发起排除", AuditEvent{EventName: "ListInstances", IPAddress: "10.1.2.3"}, false},
		{"内网172.16段发起排除", AuditEvent{EventName: "ListInstances", IPAddress: "172.20.0.1"}, false},
		{"172.15不属内网保留", AuditEvent{EventName: "ListInstances", IPAddress: "172.15.0.1"}, true},
		{"内网192.168段发起排除", AuditEvent{EventName: "ListInstances", IPAddress: "192.168.1.1"}, false},
		{"CGNAT 100.64段排除", AuditEvent{EventName: "ListInstances", IPAddress: "100.100.0.1"}, false},
		{"100.128不属CGNAT保留", AuditEvent{EventName: "ListInstances", IPAddress: "100.128.0.1"}, true},
		{"IP为空保留(控制面事件)", AuditEvent{EventName: "TerminateInstance", IPAddress: ""}, true},
		{"IP不可解析保留", AuditEvent{EventName: "ListInstances", IPAddress: "not-an-ip"}, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := keepAuditEvent(tc.event); got != tc.keep {
				t.Fatalf("keepAuditEvent(%+v) = %v, want %v", tc.event, got, tc.keep)
			}
		})
	}
}

func TestAuditCursorAdvance(t *testing.T) {
	now := time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC)
	base := AuditCursor{
		Start:       now.Add(-24 * time.Hour),
		End:         now,
		WindowHours: 24,
		Q:           "kw",
	}
	cases := []struct {
		name      string
		cur       AuditCursor
		empty     bool
		wantHours int
		wantDone  bool
	}{
		{"有事件重置 24h 窗", AuditCursor{Start: base.Start, End: base.End, WindowHours: 96}, false, 24, false},
		{"空窗倍增", base, true, 48, false},
		{"倍增封顶 336h(14 天查询窗硬限)", AuditCursor{Start: base.Start, End: base.End, WindowHours: 256}, true, 336, false},
		{"窗宽缺省按 24h 起算", AuditCursor{Start: base.Start, End: base.End}, true, 48, false},
		{"回退模式有事件重置 1h 基准窗", AuditCursor{Start: base.Start, End: base.End, WindowHours: 8, M: auditModeFallback}, false, 1, false},
		{"回退模式空窗照常倍增", AuditCursor{Start: base.Start, End: base.End, WindowHours: 1, M: auditModeFallback}, true, 2, false},
		{"越过保留期即尽头", AuditCursor{Start: now.AddDate(0, 0, -366), End: now.AddDate(0, 0, -365), WindowHours: 24}, false, 0, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			next, done := tc.cur.advance(now, tc.empty)
			if done != tc.wantDone {
				t.Fatalf("done = %v, want %v", done, tc.wantDone)
			}
			if done {
				return
			}
			if next.WindowHours != tc.wantHours {
				t.Fatalf("WindowHours = %d, want %d", next.WindowHours, tc.wantHours)
			}
			if !next.End.Equal(tc.cur.Start) {
				t.Fatalf("新窗 End = %v, 应紧邻上窗 Start %v", next.End, tc.cur.Start)
			}
			if got := next.End.Sub(next.Start); got != time.Duration(tc.wantHours)*time.Hour {
				t.Fatalf("窗宽 = %v, want %dh", got, tc.wantHours)
			}
			if next.Page != "" {
				t.Fatalf("新窗应清空窗内游标, got %q", next.Page)
			}
			if next.Q != tc.cur.Q {
				t.Fatalf("新窗应继承检索关键字, got %q want %q", next.Q, tc.cur.Q)
			}
			if next.M != tc.cur.M {
				t.Fatalf("新窗应继承通道模式, got %q want %q", next.M, tc.cur.M)
			}
		})
	}
}
