package auth_test

import (
	"encoding/json"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/auth"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	. "github.com/tidbcloud/AgentTeam/internal/api/testkit"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/mailer"
	"github.com/tidbcloud/AgentTeam/internal/oidc"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
)

const oidcTestRedirectURI = "https://agentteam.test/auth/oidc/callback"

type oidcAPITestServer struct {
	server       *httptest.Server
	clientID     string
	clientSecret string
	authCode     string
	codeVerifier string
	accessToken  string
	userInfo     oidc.UserInfo
}

func newOIDCAPITestServer(t *testing.T, userInfo oidc.UserInfo) *oidcAPITestServer {
	t.Helper()

	state := &oidcAPITestServer{
		clientID:     "oidc-test-client-id",
		clientSecret: "oidc-test-client-secret",
		authCode:     "oidc-test-auth-code",
		codeVerifier: "oidc-test-code-verifier",
		accessToken:  "oidc-test-access-token",
		userInfo:     userInfo,
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/.well-known/openid-configuration", func(writer http.ResponseWriter, request *http.Request) {
		if request.Method != http.MethodGet {
			t.Errorf("unexpected discovery method: %s", request.Method)
			http.Error(writer, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		writeOIDCTestJSON(t, writer, oidc.Discovery{
			Issuer:                state.server.URL,
			AuthorizationEndpoint: state.server.URL + "/oauth/authorize",
			TokenEndpoint:         state.server.URL + "/oauth/token",
			UserInfoEndpoint:      state.server.URL + "/oauth/userinfo",
			JWKSURI:               state.server.URL + "/oauth/jwks",
			ScopesSupported:       []string{"openid", "profile", "email", "phone"},
		})
	})
	mux.HandleFunc("/oauth/token", func(writer http.ResponseWriter, request *http.Request) {
		if request.Method != http.MethodPost {
			t.Errorf("unexpected token method: %s", request.Method)
			http.Error(writer, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		if err := request.ParseForm(); err != nil {
			t.Errorf("parse token form: %v", err)
			http.Error(writer, "bad request", http.StatusBadRequest)
			return
		}
		if got := request.FormValue("grant_type"); got != "authorization_code" {
			t.Errorf("unexpected grant_type: %q", got)
			http.Error(writer, "bad request", http.StatusBadRequest)
			return
		}
		if got := request.FormValue("client_id"); got != state.clientID {
			t.Errorf("unexpected client_id: %q", got)
			http.Error(writer, "bad request", http.StatusBadRequest)
			return
		}
		if got := request.FormValue("client_secret"); got != state.clientSecret {
			t.Errorf("unexpected client_secret: %q", got)
			http.Error(writer, "bad request", http.StatusBadRequest)
			return
		}
		if got := request.FormValue("code"); got != state.authCode {
			t.Errorf("unexpected code: %q", got)
			http.Error(writer, "bad request", http.StatusBadRequest)
			return
		}
		if got := request.FormValue("code_verifier"); got != state.codeVerifier {
			t.Errorf("unexpected code_verifier: %q", got)
			http.Error(writer, "bad request", http.StatusBadRequest)
			return
		}
		if got := request.FormValue("redirect_uri"); got != oidcTestRedirectURI {
			t.Errorf("unexpected redirect_uri: %q", got)
			http.Error(writer, "bad request", http.StatusBadRequest)
			return
		}
		writeOIDCTestJSON(t, writer, oidc.TokenResponse{
			AccessToken: state.accessToken,
			TokenType:   "Bearer",
			ExpiresIn:   3600,
			IDToken:     "oidc-test-id-token",
		})
	})
	mux.HandleFunc("/oauth/userinfo", func(writer http.ResponseWriter, request *http.Request) {
		if request.Method != http.MethodGet {
			t.Errorf("unexpected userinfo method: %s", request.Method)
			http.Error(writer, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		if got := request.Header.Get("Authorization"); got != "Bearer "+state.accessToken {
			t.Errorf("unexpected authorization header: %q", got)
			http.Error(writer, "unauthorized", http.StatusUnauthorized)
			return
		}
		writeOIDCTestJSON(t, writer, state.userInfo)
	})

	state.server = httptest.NewServer(mux)
	return state
}

func (server *oidcAPITestServer) URL() string {
	return server.server.URL
}

func (server *oidcAPITestServer) Close() {
	server.server.Close()
}

func writeOIDCTestJSON(t *testing.T, writer http.ResponseWriter, payload any) {
	t.Helper()

	writer.Header().Set("Content-Type", "application/json")
	require.NoError(t, json.NewEncoder(writer).Encode(payload))
}

func configureOIDCForTests(runtime *Runtime, oidcServer *oidcAPITestServer) {
	runtime.PingCAPAuthConfig = auth.PingCAPAuthConfig{
		Enabled:      true,
		Issuer:       oidcServer.URL(),
		ClientID:     oidcServer.clientID,
		ClientSecret: oidcServer.clientSecret,
		RedirectURI:  oidcTestRedirectURI,
		Scopes:       []string{"openid", "profile", "email", "phone"},
	}
}

func TestAuthOIDCCallbackAutoJoinsFeishuOrganizationByVerifiedEmail(t *testing.T) {
	t.Parallel()

	feishu := NewFeishuAPITestServer(t)
	defer feishu.Close()

	oidcServer := newOIDCAPITestServer(t, oidc.UserInfo{
		OpenID:        "pingcap-carol",
		DisplayName:   "Carol PingCAP",
		Email:         "carol@example.com",
		EmailVerified: true,
		AvatarURL:     "https://example.com/carol.png",
	})
	defer oidcServer.Close()

	runtime := StartRuntimeWithOrganizationProvider(t, false, 30, func(logger *slog.Logger) mailer.Mailer {
		return mailer.NewNoop(logger, "http://localhost:3001")
	}, func(pool coredb.DB, dataStore *Store) OrganizationProvider {
		return organization.NewWithDBOptions(pool, NewOrganizationCoreBridge(pool, dataStore), organization.ServiceOptions{
			FeishuBaseURL:                 feishu.URL(),
			FeishuHTTPClient:              feishu.Client(),
			FeishuCredentialEncryptionKey: TestFeishuCredentialEncryptionKey(),
		})
	})
	configureOIDCForTests(&runtime, oidcServer)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	ownerAuth := RegisterUserForOrganizationTests(t, client, server.URL, "owner@example.com", "owner")

	orgSummary := FirstOrganizationForTests(t, client, server.URL, ownerAuth.AccessToken)
	response := (*http.Response)(nil)

	PutOrganizationFeishuCredentialsForTests(t, client, server.URL, ownerAuth.AccessToken, orgSummary.ID, feishu.AppID(), feishu.AppSecret())

	var settings organization.OrganizationSettingsView
	response = DoJSON(t, client, http.MethodPatch, server.URL+"/api/v1/orgs/"+orgSummary.ID+"/settings", map[string]any{
		"directoryBackend":  "external_authority",
		"authProvider":      "feishu",
		"directoryProvider": "feishu",
	}, AuthHeaders(ownerAuth.AccessToken), &settings)
	require.Equal(t, http.StatusOK, response.StatusCode)

	syncFeishuDirectoryForAuthTests(t, client, server.URL, ownerAuth.AccessToken, orgSummary.ID)

	carolAuth := RegisterUserForOrganizationTests(t, client, server.URL, "carol@example.com", "carol")

	var orgsBefore []organization.OrganizationSummary
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/orgs", nil, AuthHeaders(carolAuth.AccessToken), &orgsBefore)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotContains(t, OrganizationIDsFromSummaries(orgsBefore), orgSummary.ID)

	var oidcAuth dto.AuthTokensResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/oidc/callback", map[string]any{
		"code":          oidcServer.authCode,
		"code_verifier": oidcServer.codeVerifier,
	}, nil, &oidcAuth)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, carolAuth.User.ID, oidcAuth.User.ID)
	require.True(t, oidcAuth.User.EmailVerified)

	var orgsAfter []organization.OrganizationSummary
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/orgs", nil, AuthHeaders(oidcAuth.AccessToken), &orgsAfter)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Contains(t, OrganizationIDsFromSummaries(orgsAfter), orgSummary.ID)

	var reboundUserID string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT user_id
FROM organization_members
WHERE organization_id = $1
  AND external_provider = 'feishu'
  AND external_id = 'ou-carol'
`, orgSummary.ID).Scan(&reboundUserID))
	require.Equal(t, carolAuth.User.ID, reboundUserID)

	var pingcapSub string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COALESCE(pingcap_sub, '')
FROM users
WHERE id = $1
`, carolAuth.User.ID).Scan(&pingcapSub))
	require.Equal(t, oidcServer.userInfo.OpenID, pingcapSub)

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/orgs/"+orgSummary.ID+"/settings", nil, AuthHeaders(ownerAuth.AccessToken), &settings)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotNil(t, settings.Feishu.LastAutoJoin)
	require.Equal(t, "success", settings.Feishu.LastAutoJoin.Status)
	require.Equal(t, "email", settings.Feishu.LastAutoJoin.SubjectType)
}

func TestAuthOIDCCallbackDoesNotAutoJoinWhenEmailRemainsUnverified(t *testing.T) {
	t.Parallel()

	feishu := NewFeishuAPITestServer(t)
	defer feishu.Close()

	oidcServer := newOIDCAPITestServer(t, oidc.UserInfo{
		OpenID:        "pingcap-carol-unverified",
		DisplayName:   "Carol PingCAP",
		Email:         "carol@example.com",
		EmailVerified: false,
		AvatarURL:     "https://example.com/carol.png",
	})
	defer oidcServer.Close()

	runtime := StartRuntimeWithOrganizationProvider(t, true, 30, func(logger *slog.Logger) mailer.Mailer {
		return mailer.NewNoop(logger, "http://localhost:3001")
	}, func(pool coredb.DB, dataStore *Store) OrganizationProvider {
		return organization.NewWithDBOptions(pool, NewOrganizationCoreBridge(pool, dataStore), organization.ServiceOptions{
			FeishuBaseURL:                 feishu.URL(),
			FeishuHTTPClient:              feishu.Client(),
			FeishuCredentialEncryptionKey: TestFeishuCredentialEncryptionKey(),
		})
	})
	configureOIDCForTests(&runtime, oidcServer)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	ownerAuth := RegisterUserForOrganizationTests(t, client, server.URL, "owner@example.com", "owner")

	orgSummary := FirstOrganizationForTests(t, client, server.URL, ownerAuth.AccessToken)
	response := (*http.Response)(nil)

	PutOrganizationFeishuCredentialsForTests(t, client, server.URL, ownerAuth.AccessToken, orgSummary.ID, feishu.AppID(), feishu.AppSecret())

	var settings organization.OrganizationSettingsView
	response = DoJSON(t, client, http.MethodPatch, server.URL+"/api/v1/orgs/"+orgSummary.ID+"/settings", map[string]any{
		"directoryBackend":  "external_authority",
		"authProvider":      "feishu",
		"directoryProvider": "feishu",
	}, AuthHeaders(ownerAuth.AccessToken), &settings)
	require.Equal(t, http.StatusOK, response.StatusCode)

	syncFeishuDirectoryForAuthTests(t, client, server.URL, ownerAuth.AccessToken, orgSummary.ID)

	carolAuth := RegisterUserForOrganizationTests(t, client, server.URL, "carol@example.com", "carol")
	require.False(t, carolAuth.User.EmailVerified)

	var oidcAuth dto.AuthTokensResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/oidc/callback", map[string]any{
		"code":          oidcServer.authCode,
		"code_verifier": oidcServer.codeVerifier,
	}, nil, &oidcAuth)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, carolAuth.User.ID, oidcAuth.User.ID)
	require.False(t, oidcAuth.User.EmailVerified)

	orgsAfter, err := runtime.Organizations.ListOrganizationsForUser(runtime.Ctx, oidcAuth.User.ID)
	require.NoError(t, err)
	require.NotContains(t, OrganizationIDsFromSummaries(orgsAfter), orgSummary.ID)

	var reboundUserID string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT user_id
FROM organization_members
WHERE organization_id = $1
  AND external_provider = 'feishu'
  AND external_id = 'ou-carol'
`, orgSummary.ID).Scan(&reboundUserID))
	require.NotEqual(t, carolAuth.User.ID, reboundUserID)

	var pingcapSub string
	var hasVerifiedEmail bool
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COALESCE(pingcap_sub, ''), email_verified_at IS NOT NULL
FROM users
WHERE id = $1
`, carolAuth.User.ID).Scan(&pingcapSub, &hasVerifiedEmail))
	require.Equal(t, oidcServer.userInfo.OpenID, pingcapSub)
	require.False(t, hasVerifiedEmail)

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/orgs/"+orgSummary.ID+"/settings", nil, AuthHeaders(ownerAuth.AccessToken), &settings)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Nil(t, settings.Feishu.LastAutoJoin)
}

func TestAuthOIDCCallbackCreatesPhoneOnlyUserWhenPhoneIsVerified(t *testing.T) {
	t.Parallel()

	oidcServer := newOIDCAPITestServer(t, oidc.UserInfo{
		OpenID:              "pingcap-phone-only",
		DisplayName:         "Phone Only",
		PhoneNumber:         "+86 138 0013 8000",
		PhoneNumberVerified: true,
		AvatarURL:           "https://example.com/phone-only.png",
	})
	defer oidcServer.Close()

	runtime := StartRuntimeWithEmailVerificationRequired(t, true)
	configureOIDCForTests(&runtime, oidcServer)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var oidcAuth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/oidc/callback", map[string]any{
		"code":          oidcServer.authCode,
		"code_verifier": oidcServer.codeVerifier,
	}, nil, &oidcAuth)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, "", oidcAuth.User.Email)
	require.False(t, oidcAuth.User.EmailVerified)
	require.Equal(t, "+8613800138000", oidcAuth.User.Mobile)
	require.True(t, oidcAuth.User.MobileVerified)

	var emailIsNull bool
	var storedMobile string
	var mobileVerified bool
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT email IS NULL, COALESCE(mobile, ''), mobile_verified_at IS NOT NULL
FROM users
WHERE id = $1
`, oidcAuth.User.ID).Scan(&emailIsNull, &storedMobile, &mobileVerified))
	require.True(t, emailIsNull)
	require.Equal(t, "+8613800138000", storedMobile)
	require.True(t, mobileVerified)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(oidcAuth.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
}
