package router

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

	"hyapp-admin-server/internal/config"
	"hyapp-admin-server/internal/modules/appuser"
	"hyapp-admin-server/internal/modules/externaladmin"
	"hyapp-admin-server/internal/modules/opscenter"
	"hyapp-admin-server/internal/repository"
	"hyapp-admin-server/internal/service"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/gin-gonic/gin"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func TestOpsCenterAppBootstrapDoesNotRequireAppCode(t *testing.T) {
	gin.SetMode(gin.TestMode)
	auth := service.NewAuthService("ops-center-router-test-secret", time.Hour)
	token, _, err := auth.GenerateAccessToken(7, "operator", nil)
	if err != nil {
		t.Fatalf("generate access token: %v", err)
	}
	sqlDB, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock: %v", err)
	}
	defer sqlDB.Close()
	gormDB, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{})
	if err != nil {
		t.Fatalf("create gorm db: %v", err)
	}
	store := repository.New(gormDB)
	mock.ExpectQuery("(?s)SELECT admin_users\\.username,.*FROM admin_users.*WHERE admin_users\\.id = \\?.*ORDER BY admin_permissions\\.code ASC").
		WithArgs(uint(7)).
		WillReturnRows(sqlmock.NewRows([]string{"username", "status", "permission_code"}).AddRow("operator", "active", nil))

	engine := New(config.Config{}, auth, store, Handlers{OpsCenter: opscenter.New(nil, nil)})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/admin/ops-center/apps", nil)
	request.Header.Set("Authorization", "Bearer "+token)
	response := httptest.NewRecorder()
	engine.ServeHTTP(response, request)

	// Ops Center 先读取跨 App 目录，再按 app_code 查询每个应用的数据；启动接口若要求
	// X-App-Code，就会形成“先拿到 App 才能发 header、先发 header 才能拿 App”的死锁。
	if response.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
	}
	if body := response.Body.String(); !strings.Contains(body, `"app_code":"yumi"`) || !strings.Contains(body, `"app_code":"aslan"`) {
		t.Fatalf("bootstrap app catalog missing fixed apps: %s", body)
	}

	// 从 appProtected 移出只解除 App 选择依赖；路由仍必须位于 protected 下，不能变成匿名应用目录。
	unauthorized := httptest.NewRecorder()
	engine.ServeHTTP(unauthorized, httptest.NewRequest(http.MethodGet, "/api/v1/admin/ops-center/apps", nil))
	if unauthorized.Code != http.StatusUnauthorized {
		t.Fatalf("unauthorized status = %d body=%s", unauthorized.Code, unauthorized.Body.String())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("authorization sql expectations: %v", err)
	}
}

func TestExternalPortalRegistersOnlyExplicitAppUserWhitelist(t *testing.T) {
	gin.SetMode(gin.TestMode)
	auth := service.NewAuthService("external-router-test-secret", time.Hour)
	externalHandler := externaladmin.New(nil, nil, externaladmin.Config{}, nil)
	engine := New(config.Config{}, auth, nil, Handlers{
		ExternalAdmin: externalHandler,
		AppUser:       &appuser.Handler{},
	})

	// The listed route exists and is protected by the isolated opaque session middleware.
	allowed := httptest.NewRecorder()
	engine.ServeHTTP(allowed, httptest.NewRequest(http.MethodGet, "/api/v1/external/app/users", nil))
	if allowed.Code != http.StatusUnauthorized {
		t.Fatalf("whitelisted route status = %d body=%s", allowed.Code, allowed.Body.String())
	}
	superAdmins := httptest.NewRecorder()
	engine.ServeHTTP(superAdmins, httptest.NewRequest(http.MethodGet, "/api/v1/external/admin/external-super-admins", nil))
	if superAdmins.Code != http.StatusUnauthorized {
		t.Fatalf("external SuperAdmin route must exist behind external session auth: status=%d body=%s", superAdmins.Code, superAdmins.Body.String())
	}

	for _, path := range []string{
		"/api/v1/external/admin/my-team/agencies",
		"/api/v1/external/admin/managers",
		"/api/v1/external/admin/bd-leaders",
		"/api/v1/external/app/users/1/password",
		"/api/v1/external/app/users/1/access-token",
		"/api/v1/external/exports/app-users",
	} {
		request := httptest.NewRequest(http.MethodPost, path, nil)
		response := httptest.NewRecorder()
		engine.ServeHTTP(response, request)
		if response.Code != http.StatusNotFound {
			t.Fatalf("non-whitelisted route %s status = %d body=%s", path, response.Code, response.Body.String())
		}
	}
}

func TestRouterDoesNotTrustForwardedIPFromDirectClient(t *testing.T) {
	gin.SetMode(gin.TestMode)
	engine := New(config.Config{TrustedProxies: []string{"127.0.0.0/8", "::1"}}, nil, nil, Handlers{})
	engine.GET("/__test_client_ip", func(c *gin.Context) {
		c.String(http.StatusOK, c.ClientIP())
	})

	// A direct caller is outside the configured ingress hop. Its XFF value must be
	// ignored, otherwise it can rotate a forged address to bypass external-login limits.
	directRequest := httptest.NewRequest(http.MethodGet, "/__test_client_ip", nil)
	directRequest.RemoteAddr = "198.51.100.24:43210"
	directRequest.Header.Set("X-Forwarded-For", "203.0.113.99")
	directResponse := httptest.NewRecorder()
	engine.ServeHTTP(directResponse, directRequest)
	if got := directResponse.Body.String(); got != "198.51.100.24" {
		t.Fatalf("direct client IP = %q, want socket peer", got)
	}

	// The local Nginx hop is trusted, but Gin must stop at the right-most untrusted
	// address. A prepended attacker-controlled value therefore cannot become identity.
	proxiedRequest := httptest.NewRequest(http.MethodGet, "/__test_client_ip", nil)
	proxiedRequest.RemoteAddr = "127.0.0.1:43210"
	proxiedRequest.Header.Set("X-Forwarded-For", "203.0.113.99, 198.51.100.24")
	proxiedResponse := httptest.NewRecorder()
	engine.ServeHTTP(proxiedResponse, proxiedRequest)
	if got := proxiedResponse.Body.String(); got != "198.51.100.24" {
		t.Fatalf("proxied client IP = %q, want right-most untrusted hop", got)
	}
}
