package testkit

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

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

type FeishuAPITestServer 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 NewFeishuAPITestServer(t *testing.T) *FeishuAPITestServer {
	t.Helper()

	fixture := &FeishuAPITestServer{
		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("/open-apis/authen/v2/oauth/token", 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: FeishuAPIRoundTripFunc(func(request *http.Request) (*http.Response, error) {
			recorder := httptest.NewRecorder()
			handler.ServeHTTP(recorder, request)
			return recorder.Result(), nil
		}),
	}
	return fixture
}

func (fixture *FeishuAPITestServer) Close() {}

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

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

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

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

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

func (fixture *FeishuAPITestServer) 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)

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

func (fixture *FeishuAPITestServer) 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))

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

func (fixture *FeishuAPITestServer) 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"))

	WriteFeishuAPIJSON(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 *FeishuAPITestServer) 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":
		WriteFeishuAPIJSON(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":
		WriteFeishuAPIJSON(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":
		WriteFeishuAPIJSON(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":
		WriteFeishuAPIJSON(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 *FeishuAPITestServer) 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"))

	WriteFeishuAPIJSON(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 *FeishuAPITestServer) 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 *FeishuAPITestServer) 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)]...)
	}

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

func (fixture *FeishuAPITestServer) 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)
	}

	WriteFeishuAPIJSON(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 WriteFeishuAPIJSON(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))
}

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

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