package organization

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

	"github.com/stretchr/testify/require"
)

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

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

	runtime := startOrganizationServiceRuntimeWithOptions(t, ServiceOptions{
		FeishuBaseURL:    feishu.URL(),
		FeishuHTTPClient: feishu.Client(),
		FeishuAppID:      feishu.AppID(),
		FeishuAppSecret:  feishu.AppSecret(),
	})
	owner := bootstrapOrganizationAdmin(t, runtime, "owner@example.com", "owner")

	organization, err := runtime.service.CreateOrganizationForUser(runtime.ctx, owner, "Feishu Sync Org", "feishu-sync-org")
	require.NoError(t, err)

	configureFeishuProviderForTests(t, runtime, owner.ID, organization.ID, "tenant-key-1", "od-root")

	result, err := runtime.service.SyncOrganizationFeishuDirectoryForUser(runtime.ctx, owner.ID, organization.ID)
	require.NoError(t, err)
	require.NotEmpty(t, result.SyncRunID)
	require.Equal(t, 3, result.ImportedUnitCount)
	require.Equal(t, 2, result.ImportedMemberCount)
	require.Equal(t, 2, result.ImportedMembershipCount)

	units, err := runtime.service.ListOrganizationUnitsForUser(runtime.ctx, owner.ID, organization.ID)
	require.NoError(t, err)
	importedUnits := make([]OrganizationUnitView, 0, len(units))
	for _, unit := range units {
		if unit.ExternalProvider == organizationProviderFeishu {
			importedUnits = append(importedUnits, unit)
		}
	}
	require.Len(t, importedUnits, 3)

	accounts, err := runtime.service.ListOrganizationMembersForUser(runtime.ctx, owner.ID, organization.ID)
	require.NoError(t, err)
	importedAccounts := make([]OrganizationMemberView, 0, len(accounts))
	for _, account := range accounts {
		if account.ExternalProvider != organizationProviderFeishu {
			continue
		}
		importedAccounts = append(importedAccounts, account)
		require.Equal(t, organizationProviderFeishu, account.ExternalProvider)
		require.NotEmpty(t, account.LastSyncRunID)
	}
	require.Len(t, importedAccounts, 2)

	defaultWorkspace := requireDefaultOrganizationWorkspace(t, runtime, owner.ID, organization.ID)
	for _, account := range importedAccounts {
		require.Equal(t, 1, account.WorkspaceAccessCount)
		require.Len(t, account.WorkspaceAccesses, 1)
		require.Equal(t, defaultWorkspace.ID, account.WorkspaceAccesses[0].WorkspaceID)
		require.Equal(t, []string{"org-team"}, account.WorkspaceAccesses[0].Sources)
	}
}

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

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

	runtime := startOrganizationServiceRuntimeWithOptions(t, ServiceOptions{
		FeishuBaseURL:    feishu.URL(),
		FeishuHTTPClient: feishu.Client(),
		FeishuAppID:      feishu.AppID(),
		FeishuAppSecret:  feishu.AppSecret(),
	})
	owner := bootstrapOrganizationAdmin(t, runtime, "owner@example.com", "owner")

	organization, err := runtime.service.CreateOrganizationForUser(runtime.ctx, owner, "Feishu Auto Root Org", "feishu-auto-root-org")
	require.NoError(t, err)
	configureFeishuProviderAutoForTests(t, runtime, owner.ID, organization.ID)

	result, err := runtime.service.SyncOrganizationFeishuDirectoryForUser(runtime.ctx, owner.ID, organization.ID)
	require.NoError(t, err)
	require.NotEmpty(t, result.SyncRunID)
	require.Equal(t, 3, result.ImportedUnitCount)
	require.Equal(t, 2, result.ImportedMemberCount)
	require.Equal(t, 2, result.ImportedMembershipCount)
}

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

	runtime := startOrganizationServiceRuntimeWithOptions(t, ServiceOptions{
		FeishuBaseURL: "https://feishu.test",
		FeishuHTTPClient: &http.Client{
			Transport: feishuRoundTripFunc(func(_ *http.Request) (*http.Response, error) {
				panic("feishu transport panic")
			}),
		},
		FeishuAppID:     "cli_test_app",
		FeishuAppSecret: "feishu-secret-123",
	})
	owner := bootstrapOrganizationAdmin(t, runtime, "owner@example.com", "owner")

	organization, err := runtime.service.CreateOrganizationForUser(runtime.ctx, owner, "Feishu Panic Org", "feishu-panic-org")
	require.NoError(t, err)
	configureFeishuProviderForTests(t, runtime, owner.ID, organization.ID, "tenant-key-1", "od-root")

	summary, created, err := runtime.service.StartOrganizationFeishuDirectorySyncForUser(runtime.ctx, owner.ID, organization.ID)
	require.NoError(t, err)
	require.True(t, created)
	require.Equal(t, organizationDirectorySyncRunStatusRunning, summary.Status)

	_, err = runtime.service.RunOrganizationFeishuDirectorySyncForUser(runtime.ctx, owner.ID, organization.ID, summary.SyncRunID)
	require.Error(t, err)
	require.Contains(t, err.Error(), "organization feishu directory sync panic")
	require.Contains(t, err.Error(), "feishu transport panic")

	detail, err := runtime.service.GetOrganizationDirectorySyncRunForUser(runtime.ctx, owner.ID, organization.ID, summary.SyncRunID)
	require.NoError(t, err)
	require.Equal(t, organizationDirectorySyncRunStatusFailed, detail.Status)
	require.NotNil(t, detail.CompletedAt)
}

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

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

	client, err := NewFeishuDirectoryHTTPClient(FeishuDirectoryHTTPClientOptions{
		BaseURL:    feishu.URL(),
		AppID:      feishu.AppID(),
		AppSecret:  feishu.AppSecret(),
		HTTPClient: feishu.Client(),
	})
	require.NoError(t, err)

	scopes, err := client.ListAuthorizedDepartmentScopes(context.Background())
	require.NoError(t, err)
	require.Equal(t, []FeishuDepartmentScope{
		{DepartmentID: "od-eng"},
		{DepartmentID: "od-people"},
	}, scopes)
}

func configureFeishuProviderForTests(t *testing.T, runtime serviceTestRuntime, ownerUserID string, organizationID string, tenantKey string, rootDepartmentID string) {
	t.Helper()

	directoryBackend := organizationDirectoryBackendExternalAuthority
	authProvider := organizationProviderFeishu
	directoryProvider := organizationProviderFeishu
	_, err := runtime.service.PatchOrganizationSettingsForUser(runtime.ctx, ownerUserID, organizationID, UpdateOrganizationSettingsParams{
		DirectoryBackend:  &directoryBackend,
		AuthProvider:      &authProvider,
		DirectoryProvider: &directoryProvider,
		Feishu: &UpdateOrganizationFeishuProviderSettingsParams{
			TenantKey:        stringPointer(tenantKey),
			RootDepartmentID: stringPointer(rootDepartmentID),
		},
	})
	require.NoError(t, err)
}

func configureFeishuProviderAutoForTests(t *testing.T, runtime serviceTestRuntime, ownerUserID string, organizationID string) {
	t.Helper()

	directoryBackend := organizationDirectoryBackendExternalAuthority
	authProvider := organizationProviderFeishu
	directoryProvider := organizationProviderFeishu
	_, err := runtime.service.PatchOrganizationSettingsForUser(runtime.ctx, ownerUserID, organizationID, UpdateOrganizationSettingsParams{
		DirectoryBackend:  &directoryBackend,
		AuthProvider:      &authProvider,
		DirectoryProvider: &directoryProvider,
	})
	require.NoError(t, err)
}

type feishuHTTPTestServer struct {
	t                     *testing.T
	baseURL               string
	client                *http.Client
	appID                 string
	appSecret             string
	token                 string
	authCode              string
	userToken             string
	batchGetUsersByEmail  map[string][]map[string]any
	batchGetUsersByMobile map[string][]map[string]any
}

func newFeishuHTTPTestServer(t *testing.T) *feishuHTTPTestServer {
	t.Helper()

	fixture := &feishuHTTPTestServer{
		t:         t,
		appID:     "cli_test_app",
		appSecret: "feishu-secret-123",
		token:     "tenant-token-123",
		authCode:  "feishu-auth-code-123",
		userToken: "feishu-user-token-123",
		batchGetUsersByEmail: map[string][]map[string]any{
			"alice@example.com": {{
				"user_id": "ou-alice",
				"email":   "alice@example.com",
			}},
			"carol@example.com": {{
				"user_id": "ou-carol",
				"email":   "carol@example.com",
			}},
		},
		batchGetUsersByMobile: map[string][]map[string]any{
			"+8613800000000": {{
				"user_id": "ou-carol",
				"mobile":  "+8613800000000",
			}},
		},
	}

	handler := http.NewServeMux()
	handler.HandleFunc("/open-apis/auth/v3/tenant_access_token/internal", fixture.handleTenantAccessToken)
	handler.HandleFunc(feishuAuthTokenPath, fixture.handleAuthAccessToken)
	handler.HandleFunc("/open-apis/authen/v1/user_info", fixture.handleAuthUserInfo)
	handler.HandleFunc("/open-apis/contact/v3/scopes", fixture.handleScopes)
	handler.HandleFunc("/open-apis/contact/v3/departments/", fixture.handleDepartments)
	handler.HandleFunc("/open-apis/contact/v3/users/batch_get_id", fixture.handleBatchGetUserIDs)
	handler.HandleFunc("/open-apis/contact/v3/users/find_by_department", fixture.handleUsersByDepartment)
	fixture.baseURL = "https://feishu.test"
	fixture.client = &http.Client{
		Transport: feishuRoundTripFunc(func(request *http.Request) (*http.Response, error) {
			recorder := httptest.NewRecorder()
			handler.ServeHTTP(recorder, request)
			return recorder.Result(), nil
		}),
	}
	return fixture
}

func (fixture *feishuHTTPTestServer) Close() {}

func (fixture *feishuHTTPTestServer) URL() string {
	return fixture.baseURL
}

func (fixture *feishuHTTPTestServer) Client() *http.Client {
	return fixture.client
}

func (fixture *feishuHTTPTestServer) AppID() string {
	return fixture.appID
}

func (fixture *feishuHTTPTestServer) AppSecret() string {
	return fixture.appSecret
}

func (fixture *feishuHTTPTestServer) AuthCode() string {
	return fixture.authCode
}

func (fixture *feishuHTTPTestServer) handleTenantAccessToken(w http.ResponseWriter, r *http.Request) {
	require.Equal(fixture.t, http.MethodPost, r.Method)

	var body struct {
		AppID     string `json:"app_id"`
		AppSecret string `json:"app_secret"`
	}
	require.NoError(fixture.t, json.NewDecoder(r.Body).Decode(&body))
	require.Equal(fixture.t, fixture.appID, body.AppID)
	require.Equal(fixture.t, fixture.appSecret, body.AppSecret)

	writeFeishuJSON(fixture.t, w, map[string]any{
		"code":                0,
		"msg":                 "ok",
		"tenant_access_token": fixture.token,
		"expire":              7200,
	})
}

func (fixture *feishuHTTPTestServer) handleAuthAccessToken(w http.ResponseWriter, r *http.Request) {
	require.Equal(fixture.t, http.MethodPost, r.Method)
	require.Empty(fixture.t, strings.TrimSpace(r.Header.Get("Authorization")))

	var body struct {
		GrantType    string `json:"grant_type"`
		ClientID     string `json:"client_id"`
		ClientSecret string `json:"client_secret"`
		Code         string `json:"code"`
		RedirectURI  string `json:"redirect_uri"`
	}
	require.NoError(fixture.t, json.NewDecoder(r.Body).Decode(&body))
	require.Equal(fixture.t, "authorization_code", body.GrantType)
	require.Equal(fixture.t, fixture.appID, body.ClientID)
	require.Equal(fixture.t, fixture.appSecret, body.ClientSecret)
	require.Equal(fixture.t, fixture.authCode, body.Code)
	require.NotEmpty(fixture.t, strings.TrimSpace(body.RedirectURI))

	writeFeishuJSON(fixture.t, w, map[string]any{
		"code":         0,
		"access_token": fixture.userToken,
		"token_type":   "Bearer",
		"expires_in":   7200,
	})
}

func (fixture *feishuHTTPTestServer) handleAuthUserInfo(w http.ResponseWriter, r *http.Request) {
	require.Equal(fixture.t, http.MethodGet, r.Method)
	require.Equal(fixture.t, "Bearer "+fixture.userToken, r.Header.Get("Authorization"))

	writeFeishuJSON(fixture.t, w, map[string]any{
		"code": 0,
		"msg":  "ok",
		"data": map[string]any{
			"user_id":    "ou-carol",
			"open_id":    "ou-open-carol",
			"union_id":   "on-union-carol",
			"name":       "Carol",
			"email":      "",
			"avatar_url": "https://example.com/carol.png",
			"tenant_key": "tenant-key-1",
		},
	})
}

func (fixture *feishuHTTPTestServer) handleDepartments(w http.ResponseWriter, r *http.Request) {
	require.Equal(fixture.t, "Bearer "+fixture.token, r.Header.Get("Authorization"))
	require.Equal(fixture.t, "open_department_id", r.URL.Query().Get("department_id_type"))
	require.Equal(fixture.t, "user_id", r.URL.Query().Get("user_id_type"))

	path := strings.TrimPrefix(r.URL.Path, "/open-apis/contact/v3/departments/")
	switch {
	case path == "0":
		writeFeishuJSON(fixture.t, w, map[string]any{
			"code": 0,
			"msg":  "success",
			"data": map[string]any{
				"department": map[string]any{
					"name":                 "Company",
					"parent_department_id": "0",
					"open_department_id":   "od-root",
					"status": map[string]any{
						"is_deleted": false,
					},
				},
			},
		})
	case path == "od-root":
		writeFeishuJSON(fixture.t, w, map[string]any{
			"code": 0,
			"msg":  "success",
			"data": map[string]any{
				"department": map[string]any{
					"name":                 "Company",
					"parent_department_id": "0",
					"open_department_id":   "od-root",
					"status": map[string]any{
						"is_deleted": false,
					},
				},
			},
		})
	case path == "od-eng":
		writeFeishuJSON(fixture.t, w, map[string]any{
			"code": 0,
			"msg":  "success",
			"data": map[string]any{
				"department": map[string]any{
					"name":                 "Engineering",
					"parent_department_id": "od-root",
					"open_department_id":   "od-eng",
					"status": map[string]any{
						"is_deleted": false,
					},
				},
			},
		})
	case path == "od-people":
		writeFeishuJSON(fixture.t, w, map[string]any{
			"code": 0,
			"msg":  "success",
			"data": map[string]any{
				"department": map[string]any{
					"name":                 "People Ops",
					"parent_department_id": "od-root",
					"open_department_id":   "od-people",
					"status": map[string]any{
						"is_deleted": false,
					},
				},
			},
		})
	case path == "od-root/children":
		fixture.handlePaginatedResponse(w, r, []map[string]any{
			{
				"name":                 "Engineering",
				"parent_department_id": "od-root",
				"open_department_id":   "od-eng",
				"status": map[string]any{
					"is_deleted": false,
				},
			},
			{
				"name":                 "People Ops",
				"parent_department_id": "od-root",
				"open_department_id":   "od-people",
				"status": map[string]any{
					"is_deleted": false,
				},
			},
		})
	case path == "od-eng/children", path == "od-people/children":
		fixture.handlePaginatedResponse(w, r, []map[string]any{})
	default:
		fixture.t.Fatalf("unexpected feishu department path: %s", r.URL.Path)
	}
}

func (fixture *feishuHTTPTestServer) handleScopes(w http.ResponseWriter, r *http.Request) {
	require.Equal(fixture.t, http.MethodGet, r.Method)
	require.Equal(fixture.t, "Bearer "+fixture.token, r.Header.Get("Authorization"))
	require.Equal(fixture.t, "open_department_id", r.URL.Query().Get("department_id_type"))

	writeFeishuJSON(fixture.t, w, map[string]any{
		"code": 0,
		"msg":  "success",
		"data": map[string]any{
			"has_more":       false,
			"page_token":     "",
			"department_ids": []string{"od-eng", "od-people"},
		},
	})
}

func (fixture *feishuHTTPTestServer) handleUsersByDepartment(w http.ResponseWriter, r *http.Request) {
	require.Equal(fixture.t, "Bearer "+fixture.token, r.Header.Get("Authorization"))
	require.Equal(fixture.t, "open_department_id", r.URL.Query().Get("department_id_type"))
	require.Equal(fixture.t, "user_id", r.URL.Query().Get("user_id_type"))

	var items []map[string]any
	switch r.URL.Query().Get("department_id") {
	case "od-root":
		items = []map[string]any{}
	case "od-eng":
		items = []map[string]any{
			{
				"user_id": "ou-alice",
				"open_id": "ou-open-alice",
				"name":    "Alice",
				"email":   "alice@example.com",
				"mobile":  "+8613800000000",
				"status": map[string]any{
					"is_frozen":    false,
					"is_resigned":  false,
					"is_activated": true,
					"is_exited":    false,
					"is_unjoin":    false,
				},
				"department_ids": []string{"od-eng"},
				"orders": []map[string]any{{
					"department_id":   "od-eng",
					"is_primary_dept": true,
				}},
			},
			{
				"user_id": "ou-bob",
				"open_id": "ou-open-bob",
				"name":    "Bob",
				"email":   "bob@example.com",
				"status": map[string]any{
					"is_frozen":    false,
					"is_resigned":  true,
					"is_activated": true,
					"is_exited":    false,
					"is_unjoin":    false,
				},
				"department_ids": []string{"od-eng"},
				"orders": []map[string]any{{
					"department_id":   "od-eng",
					"is_primary_dept": true,
				}},
			},
		}
	case "od-people":
		items = []map[string]any{
			{
				"user_id": "ou-carol",
				"open_id": "ou-open-carol",
				"name":    "Carol",
				"status": map[string]any{
					"is_frozen":    false,
					"is_resigned":  false,
					"is_activated": true,
					"is_exited":    false,
					"is_unjoin":    false,
				},
				"department_ids": []string{"od-people"},
				"orders": []map[string]any{{
					"department_id":   "od-people",
					"is_primary_dept": true,
				}},
			},
		}
	default:
		fixture.t.Fatalf("unexpected feishu department_id: %s", r.URL.Query().Get("department_id"))
	}

	fixture.handlePaginatedResponse(w, r, items)
}

func (fixture *feishuHTTPTestServer) handleBatchGetUserIDs(w http.ResponseWriter, r *http.Request) {
	require.Equal(fixture.t, http.MethodPost, r.Method)
	require.Equal(fixture.t, "Bearer "+fixture.token, r.Header.Get("Authorization"))
	require.Equal(fixture.t, "user_id", r.URL.Query().Get("user_id_type"))

	var body struct {
		Emails  []string `json:"emails"`
		Mobiles []string `json:"mobiles"`
	}
	require.NoError(fixture.t, json.NewDecoder(r.Body).Decode(&body))

	items := make([]map[string]any, 0)
	for _, email := range body.Emails {
		items = append(items, fixture.batchGetUsersByEmail[strings.TrimSpace(email)]...)
	}
	for _, mobile := range body.Mobiles {
		items = append(items, fixture.batchGetUsersByMobile[strings.TrimSpace(mobile)]...)
	}

	writeFeishuJSON(fixture.t, w, map[string]any{
		"code": 0,
		"msg":  "success",
		"data": map[string]any{
			"user_list": items,
		},
	})
}

func (fixture *feishuHTTPTestServer) handlePaginatedResponse(w http.ResponseWriter, r *http.Request, items []map[string]any) {
	fixture.t.Helper()

	pageSize := 10
	if rawPageSize := strings.TrimSpace(r.URL.Query().Get("page_size")); rawPageSize != "" {
		parsedPageSize, err := strconv.Atoi(rawPageSize)
		require.NoError(fixture.t, err)
		pageSize = parsedPageSize
	}
	offset := 0
	if rawPageToken := strings.TrimSpace(r.URL.Query().Get("page_token")); rawPageToken != "" {
		parsedOffset, err := strconv.Atoi(rawPageToken)
		require.NoError(fixture.t, err)
		offset = parsedOffset
	}

	if offset > len(items) {
		offset = len(items)
	}
	end := offset + pageSize
	if end > len(items) {
		end = len(items)
	}
	hasMore := end < len(items)
	pageToken := ""
	if hasMore {
		pageToken = fmt.Sprintf("%d", end)
	}

	writeFeishuJSON(fixture.t, w, map[string]any{
		"code": 0,
		"msg":  "success",
		"data": map[string]any{
			"has_more":   hasMore,
			"page_token": pageToken,
			"items":      items[offset:end],
		},
	})
}

func writeFeishuJSON(t *testing.T, w http.ResponseWriter, payload any) {
	t.Helper()

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

func stringPointer(value string) *string {
	return &value
}

type feishuRoundTripFunc func(request *http.Request) (*http.Response, error)

func (fn feishuRoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return fn(request)
}
