package handler

import (
	"baron-sso-backend/internal/middleware"
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"
)

// helper to build a Fiber app with the handler route mounted.
func newTestApp(h *AuthHandler) *fiber.App {
	app := fiber.New()
	app.Post("/api/v1/auth/password/reset/complete", h.CompletePasswordReset)
	return app
}

func newResetFlowTestApp(h *AuthHandler) *fiber.App {
	app := fiber.New()
	app.Post("/api/v1/auth/password/reset/verify", h.ProcessPasswordResetToken)
	app.Post("/api/v1/auth/password/reset/complete", h.CompletePasswordReset)
	return app
}

func newResetInitAppWithErrorCodeEnricher(h *AuthHandler) *fiber.App {
	app := fiber.New()
	app.Use(middleware.ErrorCodeEnricher())
	app.Post("/api/v1/auth/password/reset/init", h.InitiatePasswordReset)
	return app
}

type testRedisRepo struct {
	values map[string]string
}

func (m *testRedisRepo) Set(key string, value string, expiration time.Duration) error {
	if m.values == nil {
		m.values = map[string]string{}
	}
	m.values[key] = value
	return nil
}

func (m *testRedisRepo) Get(key string) (string, error) {
	if m.values == nil {
		return "", nil
	}
	return m.values[key], nil
}

func (m *testRedisRepo) Delete(key string) error {
	if m.values != nil {
		delete(m.values, key)
	}
	return nil
}

func (m *testRedisRepo) StoreVerificationCode(phone, code string) error {
	return m.Set("sms:"+phone, code, time.Minute)
}

func (m *testRedisRepo) GetVerificationCode(phone string) (string, error) {
	return m.Get("sms:" + phone)
}

func (m *testRedisRepo) DeleteVerificationCode(phone string) error {
	return m.Delete("sms:" + phone)
}

func TestCompletePasswordReset_MissingLoginID(t *testing.T) {
	h := &AuthHandler{}
	app := newTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"newPassword": "Password1!",
	})

	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/reset/complete", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("expected 400 for missing loginId, got %d", resp.StatusCode)
	}

	var got map[string]string
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["error"] != "Login ID and new password are required" {
		t.Fatalf("unexpected error message: %v", got["error"])
	}
}

func TestCompletePasswordReset_InvalidPasswordPolicy(t *testing.T) {
	h := &AuthHandler{}
	app := newTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"newPassword": "short", // too short + missing complexity
	})

	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/reset/complete?loginId=user@example.com", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("expected 400 for weak password, got %d", resp.StatusCode)
	}

	var got map[string]string
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["error"] != "비밀번호는 최소 12자 이상이어야 합니다" {
		t.Fatalf("unexpected error message: %v", got["error"])
	}
}

func TestCompletePasswordReset_NilIDPProvider(t *testing.T) {
	h := &AuthHandler{} // IdpProvider intentionally nil to hit the configuration error branch
	app := newTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"newPassword": "StrongPass1!",
	})

	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/reset/complete?loginId=user@example.com", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusInternalServerError {
		t.Fatalf("expected 500 when IDP provider is nil, got %d", resp.StatusCode)
	}

	var got map[string]string
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["error"] != "Authentication service not configured" {
		t.Fatalf("unexpected error message: %v", got["error"])
	}
}

func TestCompletePasswordReset_TokenValueOverridesLoginIDQuery(t *testing.T) {
	const resetToken = "tok-reset-1"
	const tokenLoginID = "user@example.com"
	const wrongLoginID = "wrong@example.com"
	const newPassword = "StrongPass1!"

	redis := &testRedisRepo{
		values: map[string]string{
			prefixPwdResetToken + resetToken: tokenLoginID,
		},
	}
	idp := &mockIdpProvider{
		userExists: true,
		err:        nil,
	}
	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  idp,
	}
	app := newResetFlowTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"newPassword": newPassword,
	})
	url := fmt.Sprintf(
		"/api/v1/auth/password/reset/complete?loginId=%s&token=%s",
		wrongLoginID,
		resetToken,
	)
	req := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	if !idp.updateCalled {
		t.Fatal("expected UpdateUserPassword to be called")
	}
	if idp.updatedLoginID != tokenLoginID {
		t.Fatalf("expected loginId from token(%s), got %s", tokenLoginID, idp.updatedLoginID)
	}
	if idp.updatedPassword != newPassword {
		t.Fatalf("expected newPassword propagated, got %s", idp.updatedPassword)
	}
}

func TestCompletePasswordReset_InvalidTokenRejectedEvenWhenLoginIDExists(t *testing.T) {
	const resetToken = "invalid-token"

	redis := &testRedisRepo{
		values: map[string]string{},
	}
	idp := &mockIdpProvider{
		userExists: true,
		err:        nil,
	}
	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  idp,
	}
	app := newResetFlowTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"newPassword": "StrongPass1!",
	})
	req := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/auth/password/reset/complete?loginId=user@example.com&token="+resetToken,
		bytes.NewReader(body),
	)
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		t.Fatalf("expected 401 for invalid token, got %d", resp.StatusCode)
	}
	if idp.updateCalled {
		t.Fatal("UpdateUserPassword must not be called when token is invalid")
	}
}

func TestCompletePasswordReset_DuplicateTokenSubmitIsIdempotent(t *testing.T) {
	const resetToken = "dup-token"
	const loginID = "user@example.com"
	const newPassword = "StrongPass1!"

	redis := &testRedisRepo{
		values: map[string]string{
			prefixPwdResetToken + resetToken: loginID,
		},
	}
	idp := &mockIdpProvider{
		userExists: true,
		err:        nil,
	}
	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  idp,
	}
	app := newResetFlowTestApp(h)

	body, _ := json.Marshal(map[string]string{
		"newPassword": newPassword,
	})
	url := fmt.Sprintf(
		"/api/v1/auth/password/reset/complete?token=%s",
		resetToken,
	)

	firstReq := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(body))
	firstReq.Header.Set("Content-Type", "application/json")
	firstResp, err := app.Test(firstReq)
	if err != nil {
		t.Fatalf("first request failed: %v", err)
	}
	defer firstResp.Body.Close()

	if firstResp.StatusCode != http.StatusOK {
		t.Fatalf("expected first response to be 200, got %d", firstResp.StatusCode)
	}
	if idp.updateCallCount != 1 {
		t.Fatalf("expected first request to update password once, got %d", idp.updateCallCount)
	}

	secondReq := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(body))
	secondReq.Header.Set("Content-Type", "application/json")
	secondResp, err := app.Test(secondReq)
	if err != nil {
		t.Fatalf("second request failed: %v", err)
	}
	defer secondResp.Body.Close()

	if secondResp.StatusCode != http.StatusOK {
		t.Fatalf("expected duplicate response to be 200, got %d", secondResp.StatusCode)
	}
	if idp.updateCallCount != 1 {
		t.Fatalf("expected duplicate request not to update password again, got %d", idp.updateCallCount)
	}
}

func TestProcessPasswordResetToken_EncodesLoginIDInRedirect(t *testing.T) {
	const token = "tok-enc"
	const loginID = "user+alias@example.com"

	t.Setenv("USERFRONT_URL", "https://sss.hmac.kr")

	redis := &testRedisRepo{
		values: map[string]string{
			prefixPwdResetToken + token: loginID,
		},
	}
	h := &AuthHandler{
		RedisService: redis,
	}
	app := newResetFlowTestApp(h)

	req := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/auth/password/reset/verify?token="+token,
		nil,
	)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusFound {
		t.Fatalf("expected 302, got %d", resp.StatusCode)
	}
	location := resp.Header.Get("Location")
	if location == "" {
		t.Fatal("missing redirect location")
	}
	redirectReq := httptest.NewRequest(http.MethodGet, location, nil)
	gotLoginID := redirectReq.URL.Query().Get("loginId")
	if gotLoginID != loginID {
		t.Fatalf("expected encoded loginId round-trip=%s, got %s (location=%s)", loginID, gotLoginID, location)
	}
}

func TestPasswordResetVerifyAlias_AcceptsShortVePath(t *testing.T) {
	const token = "tok-ve"
	const loginID = "user@example.com"

	redis := &testRedisRepo{
		values: map[string]string{
			prefixPwdResetToken + token: loginID,
		},
	}
	h := &AuthHandler{
		RedisService: redis,
	}

	app := fiber.New()
	app.Get("/api/v1/auth/password/reset/ve", h.VerifyPasswordResetPage)
	app.Post("/api/v1/auth/password/reset/ve", h.ProcessPasswordResetToken)

	getReq := httptest.NewRequest(
		http.MethodGet,
		"/api/v1/auth/password/reset/ve?token="+token,
		nil,
	)
	getResp, err := app.Test(getReq)
	if err != nil {
		t.Fatalf("get request failed: %v", err)
	}
	defer getResp.Body.Close()

	if getResp.StatusCode != http.StatusOK {
		t.Fatalf("expected alias GET to return 200, got %d", getResp.StatusCode)
	}

	postReq := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/auth/password/reset/ve?token="+token,
		nil,
	)
	postResp, err := app.Test(postReq)
	if err != nil {
		t.Fatalf("post request failed: %v", err)
	}
	defer postResp.Body.Close()

	if postResp.StatusCode != http.StatusFound {
		t.Fatalf("expected alias POST to return 302, got %d", postResp.StatusCode)
	}
}

func TestPasswordResetVerifyPathToken_AcceptsShortVPath(t *testing.T) {
	const token = "tok-path"
	const loginID = "user@example.com"

	redis := &testRedisRepo{
		values: map[string]string{
			prefixPwdResetToken + token: loginID,
		},
	}
	h := &AuthHandler{
		RedisService: redis,
	}

	app := fiber.New()
	app.Get("/api/v1/auth/password/reset/v/:token", h.VerifyPasswordResetPage)
	app.Post("/api/v1/auth/password/reset/v/:token", h.ProcessPasswordResetToken)

	getReq := httptest.NewRequest(
		http.MethodGet,
		"/api/v1/auth/password/reset/v/"+token,
		nil,
	)
	getResp, err := app.Test(getReq)
	if err != nil {
		t.Fatalf("get request failed: %v", err)
	}
	defer getResp.Body.Close()

	if getResp.StatusCode != http.StatusOK {
		t.Fatalf("expected path-token GET to return 200, got %d", getResp.StatusCode)
	}

	postReq := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/auth/password/reset/v/"+token,
		nil,
	)
	postResp, err := app.Test(postReq)
	if err != nil {
		t.Fatalf("post request failed: %v", err)
	}
	defer postResp.Body.Close()

	if postResp.StatusCode != http.StatusFound {
		t.Fatalf("expected path-token POST to return 302, got %d", postResp.StatusCode)
	}
}

func TestPasswordResetInit_LegacyErrorResponseHasCodeViaMiddleware(t *testing.T) {
	h := &AuthHandler{}
	app := newResetInitAppWithErrorCodeEnricher(h)

	body, _ := json.Marshal(map[string]string{
		"loginId": "",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/reset/init", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d", resp.StatusCode)
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if got["error"] != "Login ID is required" {
		t.Fatalf("unexpected error message: %v", got["error"])
	}
	if got["code"] != "bad_request" {
		t.Fatalf("expected code=bad_request, got %v", got["code"])
	}
}

func TestInitiatePasswordReset_SmsContainsVerifyLink(t *testing.T) {
	t.Setenv("USERFRONT_URL", "https://sss.hmac.kr")

	redis := &testRedisRepo{values: map[string]string{}}
	smsSvc := &mockSmsService{}
	h := &AuthHandler{
		RedisService: redis,
		IdpProvider:  &mockIdpProvider{},
		SmsService:   smsSvc,
	}

	app := fiber.New()
	app.Post("/api/v1/auth/password/reset/init", h.InitiatePasswordReset)

	body, _ := json.Marshal(map[string]string{
		"loginId": "01012345678",
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/reset/init", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	if !strings.Contains(smsSvc.lastContent, "/api/v1/auth/password/reset/v/") {
		t.Fatalf("expected SMS to contain short path verify link, got %q", smsSvc.lastContent)
	}
	if strings.Contains(smsSvc.lastContent, "/reset-password?token=") {
		t.Fatalf("expected direct reset-password link to be removed, got %q", smsSvc.lastContent)
	}
}
