package api

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

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

// fakeWebhookEvents 记录 webhook handler 对回传服务的调用,便于断言分派行为。
type fakeWebhookEvents struct {
	secret    string
	cfgID     uint
	confirmed []string
	ingested  []ingestCall
	ingestErr error
}

type ingestCall struct {
	cfgID     uint
	messageID string
	payload   string
	truncated bool
}

func (f *fakeWebhookEvents) ResolveSecret(_ context.Context, secret string) (uint, bool) {
	if secret == f.secret {
		return f.cfgID, true
	}
	return 0, false
}

func (f *fakeWebhookEvents) ConfirmAsync(confirmURL string) {
	f.confirmed = append(f.confirmed, confirmURL)
}

func (f *fakeWebhookEvents) Ingest(_ context.Context, cfgID uint, messageID string, payload []byte, truncated bool) error {
	if f.ingestErr != nil {
		return f.ingestErr
	}
	f.ingested = append(f.ingested, ingestCall{cfgID, messageID, string(payload), truncated})
	return nil
}

// newWebhookEnv 只挂 webhook 路由(与生产同链:Recovery+系统日志中间件),
// 返回引擎、假回传服务与系统日志服务(断言留痕脱敏用)。
func newWebhookEnv(t *testing.T) (*gin.Engine, *fakeWebhookEvents, *service.SystemLogService, *gorm.DB) {
	t.Helper()
	gin.SetMode(gin.TestMode)
	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)
	}
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("db handle: %v", err)
	}
	sqlDB.SetMaxOpenConns(1)
	if err := db.AutoMigrate(&model.SystemLog{}); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	fake := &fakeWebhookEvents{secret: strings.Repeat("ab", 32), cfgID: 7}
	systemLogs := service.NewSystemLogService(db)
	r := gin.New()
	r.POST("/api/v1/webhooks/oci-logs/:secret",
		gin.Recovery(), systemLogMiddleware(systemLogs), (&webhookHandler{events: fake}).handle)
	return r, fake, systemLogs, db
}

// postWebhook 以 ONS 头发起一次投递请求。
func postWebhook(t *testing.T, r *gin.Engine, secret, body string, headers map[string]string) int {
	t.Helper()
	req := httptestNewRequest(secret, body, headers)
	w := doRawRequest(r, req)
	return w.Code
}

func httptestNewRequest(secret, body string, headers map[string]string) *http.Request {
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/webhooks/oci-logs/"+secret, strings.NewReader(body))
	for k, v := range headers {
		req.Header.Set(k, v)
	}
	return req
}

// doRawRequest 直接以 *http.Request 走引擎,便于携带任意头。
func doRawRequest(r *gin.Engine, req *http.Request) *httptest.ResponseRecorder {
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	return w
}

func TestWebhookDispatch(t *testing.T) {
	now := time.Now().UTC().Format(time.RFC3339)
	stale := time.Now().Add(-10 * time.Minute).UTC().Format(time.RFC3339)
	oracleURL := "https://cell1.notification.ap-tokyo-1.oci.oraclecloud.com/confirm?token=x"
	tests := []struct {
		name        string
		secret      string
		headers     map[string]string
		body        string
		wantStatus  int
		wantConfirm int
		wantIngest  int
	}{
		{
			name: "secret 不匹配 404", secret: "wrong",
			headers: map[string]string{onsHeaderMessageID: "m1"}, wantStatus: http.StatusNotFound,
		},
		{
			name: "订阅确认白名单内异步激活", secret: "",
			headers:    map[string]string{onsHeaderMessageType: msgTypeSubscription, onsHeaderConfirmURL: oracleURL},
			wantStatus: http.StatusOK, wantConfirm: 1,
		},
		{
			name: "订阅确认白名单外拒绝", secret: "",
			headers:    map[string]string{onsHeaderMessageType: msgTypeSubscription, onsHeaderConfirmURL: "https://evil.example.com/confirm"},
			wantStatus: http.StatusBadRequest,
		},
		{
			name: "订阅确认伪装 host 后缀拒绝", secret: "",
			headers:    map[string]string{onsHeaderMessageType: msgTypeSubscription, onsHeaderConfirmURL: "https://eviloraclecloud.com/confirm"},
			wantStatus: http.StatusBadRequest,
		},
		{
			name: "订阅确认非 https 拒绝", secret: "",
			headers:    map[string]string{onsHeaderMessageType: msgTypeSubscription, onsHeaderConfirmURL: "http://x.oraclecloud.com/confirm"},
			wantStatus: http.StatusBadRequest,
		},
		{
			name: "退订确认仅记录", secret: "",
			headers:    map[string]string{onsHeaderMessageType: msgTypeUnsubscribe},
			wantStatus: http.StatusOK,
		},
		{
			name: "通知消息入库", secret: "",
			headers:    map[string]string{onsHeaderMessageID: "m1", onsHeaderTimestamp: now},
			body:       `{"eventType":"t"}`,
			wantStatus: http.StatusOK, wantIngest: 1,
		},
		{
			name: "ONS 无冒号时区格式放行", secret: "",
			headers: map[string]string{
				onsHeaderMessageID: "m1",
				onsHeaderTimestamp: time.Now().UTC().Format("2006-01-02T15:04:05.999") + "+0000",
			},
			wantStatus: http.StatusOK, wantIngest: 1,
		},
		{
			name: "时间戳过期拒收", secret: "",
			headers:    map[string]string{onsHeaderMessageID: "m1", onsHeaderTimestamp: stale},
			wantStatus: http.StatusForbidden,
		},
		{
			name: "时间戳非法拒收", secret: "",
			headers:    map[string]string{onsHeaderMessageID: "m1", onsHeaderTimestamp: "yesterday"},
			wantStatus: http.StatusForbidden,
		},
		{
			name: "缺 MessageId 拒收", secret: "",
			headers:    map[string]string{onsHeaderTimestamp: now},
			wantStatus: http.StatusBadRequest,
		},
		{
			name: "SigningCertURL 非 Oracle 域名拒绝", secret: "",
			headers: map[string]string{
				onsHeaderMessageID:      "m-evil-cert",
				onsHeaderTimestamp:      now,
				onsHeaderSigningCertURL: "https://evil.example.com/cert.crt",
			},
			body:       `{"eventType":"t"}`,
			wantStatus: http.StatusForbidden,
		},
		{
			name: "SigningCertURL 非 https 拒绝", secret: "",
			headers: map[string]string{
				onsHeaderMessageID:      "m-http-cert",
				onsHeaderTimestamp:      now,
				onsHeaderSigningCertURL: "http://objectstorage.eu-paris-1.oraclecloud.com/cert.crt",
			},
			body:       `{"eventType":"t"}`,
			wantStatus: http.StatusForbidden,
		},
		{
			name: "SigningCertURL 白名单内放行(canonical form 未接入,不做签名校验)", secret: "",
			headers: map[string]string{
				onsHeaderMessageID:      "m-good-cert",
				onsHeaderTimestamp:      now,
				onsHeaderSigningCertURL: "https://objectstorage.eu-paris-1.oraclecloud.com/p/x/n/bmc-ons-prod/b/ons-ds/o/sig.crt",
			},
			body:       `{"eventType":"t"}`,
			wantStatus: http.StatusOK, wantIngest: 1,
		},
		{
			name: "缺时间戳头放行入库", secret: "",
			headers:    map[string]string{onsHeaderMessageID: "m2"},
			wantStatus: http.StatusOK, wantIngest: 1,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r, fake, _, _ := newWebhookEnv(t)
			secret := tt.secret
			if secret == "" {
				secret = fake.secret
			}
			code := postWebhook(t, r, secret, tt.body, tt.headers)
			if code != tt.wantStatus {
				t.Fatalf("status = %d, want %d", code, tt.wantStatus)
			}
			if len(fake.confirmed) != tt.wantConfirm {
				t.Errorf("confirm calls = %d, want %d", len(fake.confirmed), tt.wantConfirm)
			}
			if len(fake.ingested) != tt.wantIngest {
				t.Errorf("ingest calls = %d, want %d", len(fake.ingested), tt.wantIngest)
			}
			if tt.wantIngest > 0 && fake.ingested[0].cfgID != fake.cfgID {
				t.Errorf("ingest cfg = %d, want %d", fake.ingested[0].cfgID, fake.cfgID)
			}
		})
	}
}

func TestWebhookBodyTruncation(t *testing.T) {
	r, fake, _, _ := newWebhookEnv(t)
	big := strings.Repeat("x", webhookBodyLimit+100)
	code := postWebhook(t, r, fake.secret, big, map[string]string{onsHeaderMessageID: "m-big"})
	if code != http.StatusOK {
		t.Fatalf("status = %d, want 200", code)
	}
	got := fake.ingested[0]
	if !got.truncated || len(got.payload) != webhookBodyLimit {
		t.Errorf("truncated=%v len=%d, want true/%d", got.truncated, len(got.payload), webhookBodyLimit)
	}
}

func TestWebhookSystemLogHidesSecret(t *testing.T) {
	r, fake, systemLogs, db := newWebhookEnv(t)
	postWebhook(t, r, fake.secret, "{}", map[string]string{onsHeaderMessageID: "m1"})
	systemLogs.Wait()
	var entry model.SystemLog
	if err := db.First(&entry).Error; err != nil {
		t.Fatalf("system log row: %v", err)
	}
	if strings.Contains(entry.Path, fake.secret) {
		t.Errorf("system log path leaks secret: %s", entry.Path)
	}
	if !strings.Contains(entry.Path, ":secret") {
		t.Errorf("path = %s, want route template with :secret", entry.Path)
	}
}

func TestSanitizeLogPath(t *testing.T) {
	tests := []struct {
		name string
		in   string
		want string
	}{
		{name: "webhook 路径脱敏末段", in: "/api/v1/webhooks/oci-logs/abcdef123456", want: "/api/v1/webhooks/oci-logs/***"},
		{name: "非 webhook 路径原样", in: "/api/v1/tasks/3", want: "/api/v1/tasks/3"},
		{name: "webhook 前缀本身原样", in: "/api/v1/webhooks/", want: "/api/v1/webhooks/"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := sanitizeLogPath(tt.in); got != tt.want {
				t.Errorf("sanitizeLogPath(%q) = %q, want %q", tt.in, got, tt.want)
			}
		})
	}
}
