package handlers

// P1 #2 回归测试：OAuth2 state 必须随机、回调必须校验。
// 旧实现 state 为硬编码常量且回调完全不校验（登录 CSRF / 授权码注入）。

import (
	"encoding/json"
	"html/template"
	"math"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"mail_go/config"
	"mail_go/internal/mailutil"

	"github.com/gin-gonic/gin"
)

// testTemplateFuncs 提供模板解析所需的自定义函数（与 web 包的
// templateFuncs 等价，但 handlers 包无法反向依赖 web 包）。
func testTemplateFuncs() template.FuncMap {
	return template.FuncMap{
		"add":        func(a, b int) int { return a + b },
		"sub":        func(a, b int) int { return a - b },
		"mul":        func(a, b int) int { return a * b },
		"div":        func(a, b int) int { return a / b },
		"mod":        func(a, b int) int { return a % b },
		"ceilDiv":    func(a, b int) int { return int(math.Ceil(float64(a) / float64(b))) },
		"seq":        func(n int) []int { r := make([]int, n); for i := range r { r[i] = i + 1 }; return r },
		"domainName": func(domainID uint, domains []interface{}) string { return "Domain #1" },
		"jsonify": func(v interface{}) template.JS {
			b, _ := json.Marshal(v)
			return template.JS(b)
		},
		"formatBytes": func(b int64) string {
			return "1 KB"
		},
		"decodeHeader": mailutil.DecodeRFC2047,
		"mailName":     func(s string) string { return s },
		"mailEmail":    func(s string) string { return s },
		"initial":      func(s string) string { return "?" },
		"truncate":     func(s string, n int) string { return s },
		"shortDate":    func(t time.Time) string { return t.Format("2006-01-02") },
		"avatarStyle":  func(s string) string { return "background:#eee;color:#333" },
	}
}

func newOAuth2TestContext(t *testing.T) (*gin.Context, *AuthHandler, *httptest.ResponseRecorder) {
	t.Helper()
	gin.SetMode(gin.TestMode)
	w := httptest.NewRecorder()
	c, engine := gin.CreateTestContext(w)
	// 回调的错误分支渲染 login 模板，需要加载模板及自定义函数
	tmpl := template.Must(template.New("").Funcs(testTemplateFuncs()).ParseGlob(filepath.Join("..", "templates", "*.html")))
	engine.SetHTMLTemplate(tmpl)
	c.Request = httptest.NewRequest(http.MethodGet, "/auth/oauth2", nil)

	authCfg := config.AuthConfig{
		OAuth2Enabled:      true,
		// 使用本地拒绝连接的地址作为 provider，token 交换快速失败，
		// 测试不依赖外部网络。
		OAuth2Provider:     "127.0.0.1:1",
		OAuth2ClientID:     "test-client-id",
		OAuth2ClientSecret: "test-client-secret",
		OAuth2RedirectURL:  "https://mail.example.com/auth/oauth2/callback",
	}
	h := NewAuthHandler(nil, authCfg, config.BanConfig{MaxFailAttempts: 100})
	return c, h, w
}

func TestRandomOAuth2State(t *testing.T) {
	s1, err := randomOAuth2State()
	if err != nil {
		t.Fatalf("randomOAuth2State() error: %v", err)
	}
	if len(s1) != oauth2StateRandLen*2 {
		t.Fatalf("state length = %d, want %d (hex)", len(s1), oauth2StateRandLen*2)
	}
	s2, _ := randomOAuth2State()
	if s1 == s2 {
		t.Fatal("state must be unique per request")
	}
	if s1 == "mailgo_oauth2_state" {
		t.Fatal("state must not be the old hardcoded constant")
	}
}

func TestOAuth2StartSetsRandomStateCookie(t *testing.T) {
	c, h, w := newOAuth2TestContext(t)
	h.OAuth2Start(c)

	if w.Code != http.StatusFound {
		t.Fatalf("status = %d, want 302", w.Code)
	}
	loc := w.Header().Get("Location")
	if !strings.Contains(loc, "state=") {
		t.Fatalf("redirect URL should carry state: %s", loc)
	}

	// state cookie 必须存在且与 URL 中的一致
	cookies := w.Result().Cookies()
	var stateVal string
	found := false
	for _, ck := range cookies {
		if ck.Name == oauth2StateCookie {
			found = true
			stateVal = ck.Value
			if !ck.HttpOnly {
				t.Error("state cookie must be HttpOnly")
			}
			if !ck.Secure {
				t.Error("state cookie must be Secure")
			}
			if ck.MaxAge <= 0 || ck.MaxAge > oauth2StateMaxAge {
				t.Errorf("state cookie MaxAge = %d, want in (0, %d]", ck.MaxAge, oauth2StateMaxAge)
			}
		}
	}
	if !found {
		t.Fatal("OAuth2Start should set state cookie")
	}

	u, err := url.Parse(loc)
	if err != nil {
		t.Fatalf("parse location: %v", err)
	}
	if u.Query().Get("state") != stateVal {
		t.Fatalf("cookie state %q != URL state %q", stateVal, u.Query().Get("state"))
	}

	// 两次发起的 state 不同
	c2, h2, w2 := newOAuth2TestContext(t)
	h2.OAuth2Start(c2)
	u2, _ := url.Parse(w2.Header().Get("Location"))
	if u2.Query().Get("state") == stateVal {
		t.Fatal("state must differ between sessions")
	}
}

func TestOAuth2CallbackRejectsMissingOrMismatchedState(t *testing.T) {
	cases := []struct {
		name        string
		cookieState string
		queryState  string
	}{
		{"no cookie", "", "abc"},
		{"no query state", "abc", ""},
		{"mismatch", "abc", "xyz"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			c, h, w := newOAuth2TestContext(t)
			q := url.Values{}
			q.Set("code", "test-code")
			if tc.queryState != "" {
				q.Set("state", tc.queryState)
			}
			c.Request = httptest.NewRequest(http.MethodGet, "/auth/oauth2/callback?"+q.Encode(), nil)
			if tc.cookieState != "" {
				c.Request.AddCookie(&http.Cookie{Name: oauth2StateCookie, Value: tc.cookieState})
			}
			h.OAuth2Callback(c)
			if w.Code != http.StatusForbidden {
				t.Fatalf("status = %d, want 403", w.Code)
			}
		})
	}
}

func TestOAuth2CallbackAcceptsValidState(t *testing.T) {
	// 模拟完整流程：Start 下发 state -> Callback 带回同一 state。
	// state 校验通过后应进入后续流程（本测试无真实 IdP，
	// code 交换会失败并渲染登录错误页，但这证明 state 关卡已通过）。
	c, h, w := newOAuth2TestContext(t)
	h.OAuth2Start(c)
	var stateVal string
	for _, ck := range w.Result().Cookies() {
		if ck.Name == oauth2StateCookie {
			stateVal = ck.Value
		}
	}

	w2 := httptest.NewRecorder()
	c2, engine2 := gin.CreateTestContext(w2)
	tmpl2 := template.Must(template.New("").Funcs(testTemplateFuncs()).ParseGlob(filepath.Join("..", "templates", "*.html")))
	engine2.SetHTMLTemplate(tmpl2)
	q := url.Values{}
	q.Set("code", "test-code")
	q.Set("state", stateVal)
	c2.Request = httptest.NewRequest(http.MethodGet, "/auth/oauth2/callback?"+q.Encode(), nil)
	c2.Request.AddCookie(&http.Cookie{Name: oauth2StateCookie, Value: stateVal})

	h.OAuth2Callback(c2)

	// state 校验失败返回 403；此处应为非 403（进入 token 交换失败分支）
	if w2.Code == http.StatusForbidden {
		t.Fatalf("valid state was rejected")
	}
	if !strings.Contains(w2.Body.String(), "OAuth2") {
		body := w2.Body.String()
		if len(body) > 200 {
			body = body[:200]
		}
		t.Fatalf("expected OAuth2 error page after state check passed, body: %s", body)
	}
}
