package api

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

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

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

// nullClient 是 oci.Client 的空实现，路由测试不触发任何云调用。
// 内嵌接口以自动满足 oci.Client，未覆写的方法不会被这些测试调用。
type nullClient struct{ oci.Client }

func (nullClient) ValidateKey(context.Context, oci.Credentials) (oci.TenancyInfo, error) {
	return oci.TenancyInfo{}, nil
}

func (nullClient) FetchAccountProfile(context.Context, oci.Credentials) (oci.AccountProfile, error) {
	return oci.AccountProfile{}, nil
}

func (nullClient) ListRegionSubscriptions(context.Context, oci.Credentials) ([]oci.RegionSubscription, error) {
	return nil, nil
}

func (nullClient) SubscribeRegion(context.Context, oci.Credentials, string, string) error {
	return nil
}

func (nullClient) ListLimits(context.Context, oci.Credentials, oci.LimitsQuery) ([]oci.LimitValue, error) {
	return nil, nil
}

func (nullClient) ListLimitServices(context.Context, oci.Credentials, string) ([]oci.LimitService, error) {
	return nil, nil
}

func (nullClient) ListSubscriptions(context.Context, oci.Credentials) ([]oci.SubscriptionInfo, error) {
	return nil, nil
}

func (nullClient) GetSubscription(context.Context, oci.Credentials, string) (oci.SubscriptionDetail, error) {
	return oci.SubscriptionDetail{}, nil
}

func newTestRouter(t *testing.T) (*gin.Engine, *service.AuthService, *service.SystemLogService) {
	t.Helper()
	r, auth, systemLogs, _ := newTestRouterDB(t)
	return r, auth, systemLogs
}

// newTestRouterDB 额外回传 db 句柄,供需要直插数据(如网关模型缓存)的测试使用。
func newTestRouterDB(t *testing.T) (*gin.Engine, *service.AuthService, *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)
	}
	// :memory: 库每个连接彼此独立，异步日志写入会触发池内新连接而丢表，锁单连接
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("db handle: %v", err)
	}
	sqlDB.SetMaxOpenConns(1)
	if err := db.AutoMigrate(&model.User{}, &model.OciConfig{}, &model.Task{}, &model.TaskLog{}, &model.Setting{}, &model.SystemLog{}, &model.LogEvent{}, &model.Proxy{}, &model.AiKey{}, &model.AiChannel{}, &model.AiModelCache{}, &model.AiCallLog{}); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	cipher, err := crypto.NewCipher("test-key")
	if err != nil {
		t.Fatalf("new cipher: %v", err)
	}
	auth := service.NewAuthService(db, "test-secret")
	if err := auth.EnsureAdmin("admin", "pass123"); err != nil {
		t.Fatalf("ensure admin: %v", err)
	}
	ociConfigs := service.NewOciConfigService(db, cipher, nullClient{})
	settings := service.NewSettingService(db, cipher)
	notifier := service.NewNotifier(settings)
	tasks := service.NewTaskService(db, ociConfigs, notifier, settings)
	systemLogs := service.NewSystemLogService(db)
	logEvents := service.NewLogEventService(db)
	oauth := service.NewOAuthService(db, settings, auth)
	r := NewRouter(auth, oauth, ociConfigs, tasks, service.NewConsoleService(ociConfigs), settings, notifier, systemLogs, logEvents, service.NewProxyService(db, cipher), service.NewAiGatewayService(db, ociConfigs, nullClient{}))
	return r, auth, systemLogs, db
}

func doRequest(t *testing.T, r *gin.Engine, method, path, token, body string) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(method, path, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	return w
}

func TestLoginEndpoint(t *testing.T) {
	r, _, _ := newTestRouter(t)
	tests := []struct {
		name       string
		body       string
		wantStatus int
	}{
		{name: "正确凭据", body: `{"username":"admin","password":"pass123"}`, wantStatus: http.StatusOK},
		{name: "密码错误", body: `{"username":"admin","password":"wrong"}`, wantStatus: http.StatusUnauthorized},
		{name: "缺少字段", body: `{"username":"admin"}`, wantStatus: http.StatusBadRequest},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", tt.body)
			if w.Code != tt.wantStatus {
				t.Errorf("status = %d, want %d, body %s", w.Code, tt.wantStatus, w.Body.String())
			}
			if tt.wantStatus == http.StatusOK && !strings.Contains(w.Body.String(), "token") {
				t.Errorf("body = %s, want token field", w.Body.String())
			}
		})
	}
}

func TestSecuredRoutesRequireToken(t *testing.T) {
	r, auth, _ := newTestRouter(t)
	token, _, err := auth.Login(context.Background(), "admin", "pass123", "127.0.0.1", "")
	if err != nil {
		t.Fatalf("login: %v", err)
	}
	tests := []struct {
		name       string
		token      string
		wantStatus int
	}{
		{name: "无令牌", token: "", wantStatus: http.StatusUnauthorized},
		{name: "伪造令牌", token: "forged.token.value", wantStatus: http.StatusUnauthorized},
		{name: "有效令牌", token: token, wantStatus: http.StatusOK},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := doRequest(t, r, http.MethodGet, "/api/v1/oci-configs", tt.token, "")
			if w.Code != tt.wantStatus {
				t.Errorf("status = %d, want %d, body %s", w.Code, tt.wantStatus, w.Body.String())
			}
		})
	}
}

func TestSystemLogsEndpoint(t *testing.T) {
	r, auth, logs := newTestRouter(t)
	token, _, err := auth.Login(context.Background(), "admin", "pass123", "127.0.0.1", "")
	if err != nil {
		t.Fatalf("login: %v", err)
	}
	// 经 HTTP 触发一次登录失败：埋点应异步落一条 401 记录
	doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", `{"username":"admin","password":"wrong"}`)
	logs.Wait()
	tests := []struct {
		name      string
		query     string
		wantTotal string
	}{
		{name: "无关键字返回全部", query: "", wantTotal: `"total":1`},
		{name: "关键字命中登录路径", query: "?keyword=login", wantTotal: `"total":1`},
		{name: "关键字不命中返回空", query: "?keyword=no-such", wantTotal: `"total":0`},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := doRequest(t, r, http.MethodGet, "/api/v1/system-logs"+tt.query, token, "")
			if w.Code != http.StatusOK {
				t.Fatalf("status = %d, want %d, body %s", w.Code, http.StatusOK, w.Body.String())
			}
			body := w.Body.String()
			if !strings.Contains(body, "items") || !strings.Contains(body, tt.wantTotal) {
				t.Errorf("body = %s, want items and %s", body, tt.wantTotal)
			}
		})
	}
}

// TestLoginFieldLimits 超长登录字段被绑定校验拒绝(S-03 高基数防护第一道)。
func TestLoginFieldLimits(t *testing.T) {
	r, _, _ := newTestRouter(t)
	longName := strings.Repeat("a", 65)
	longPass := strings.Repeat("b", 129)
	tests := []struct {
		name string
		body string
	}{
		{name: "用户名超长", body: `{"username":"` + longName + `","password":"x"}`},
		{name: "密码超长", body: `{"username":"admin","password":"` + longPass + `"}`},
		{name: "验证码超长", body: `{"username":"admin","password":"x","totpCode":"123456789"}`},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", tt.body)
			if w.Code != http.StatusBadRequest {
				t.Errorf("status = %d, want 400", w.Code)
			}
		})
	}
}

// TestBodyLimitRejectsHuge 面板 API 请求体超 1MB 被拒(S-03)。
func TestBodyLimitRejectsHuge(t *testing.T) {
	r, _, _ := newTestRouter(t)
	huge := `{"username":"admin","password":"` + strings.Repeat("x", 2<<20) + `"}`
	w := doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", huge)
	if w.Code != http.StatusBadRequest {
		t.Errorf("status = %d, want 400(body too large)", w.Code)
	}
}

// TestRevokeSessionsEndpoint 撤销全部会话:直接调用即生效,
// 响应带新 token,旧 token 随即失效。
func TestRevokeSessionsEndpoint(t *testing.T) {
	r, _, _ := newTestRouter(t)
	login := doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", `{"username":"admin","password":"pass123"}`)
	var sess struct{ Token string }
	if err := json.Unmarshal(login.Body.Bytes(), &sess); err != nil || sess.Token == "" {
		t.Fatalf("login: %s", login.Body.String())
	}
	w := doRequest(t, r, http.MethodPost, "/api/v1/auth/revoke-sessions", sess.Token, "")
	if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "token") {
		t.Fatalf("revoke = %d %s", w.Code, w.Body.String())
	}
	w = doRequest(t, r, http.MethodGet, "/api/v1/auth/credentials", sess.Token, "")
	if w.Code != http.StatusUnauthorized {
		t.Errorf("撤销后旧 token 访问 = %d, want 401", w.Code)
	}
}

// TestRespondErrorSanitizesInternal 内部错误边界(S-08):wrap 链细节不透传,
// 响应为固定文案 + requestId;记录缺失映射 404 固定文案。
func TestRespondErrorSanitizesInternal(t *testing.T) {
	gin.SetMode(gin.TestMode)
	cases := []struct {
		name       string
		err        error
		wantStatus int
		wantBody   string   // 必须出现
		leaks      []string // 不得出现
	}{
		{
			name:       "内部错误脱敏",
			err:        fmt.Errorf("query users: dial tcp 10.0.0.5:3306: dsn user:secretpw"),
			wantStatus: http.StatusInternalServerError,
			wantBody:   "requestId",
			leaks:      []string{"secretpw", "10.0.0.5", "dsn", "dial tcp"},
		},
		{
			name:       "记录缺失固定文案",
			err:        fmt.Errorf("find oci config 5: %w", gorm.ErrRecordNotFound),
			wantStatus: http.StatusNotFound,
			wantBody:   "资源不存在",
			leaks:      []string{"find oci config"},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			rec := httptest.NewRecorder()
			c, _ := gin.CreateTestContext(rec)
			c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/x", nil)
			respondError(c, tc.err)
			if rec.Code != tc.wantStatus {
				t.Fatalf("status = %d, want %d", rec.Code, tc.wantStatus)
			}
			body := rec.Body.String()
			if !strings.Contains(body, tc.wantBody) {
				t.Errorf("body %q 应包含 %q", body, tc.wantBody)
			}
			for _, leak := range tc.leaks {
				if strings.Contains(body, leak) {
					t.Errorf("body %q 泄露内部细节 %q", body, leak)
				}
			}
		})
	}
}
