package feishu

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"AutoSSO/internal/config"
	"AutoSSO/internal/pkg/httpclient"

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

// createTestClient creates a test client with a mock server.
func createTestClient(t *testing.T, handler http.Handler) (*Client, *httptest.Server) {
	server := httptest.NewServer(handler)

	cfg := &config.FeishuConfig{
		AppID:        "test_app_id",
		AppSecret:    "test_app_secret",
		ApprovalCode: "test_approval_code",
		ChatID:       "test_chat_id",
		BaseURL:      server.URL,
	}

	httpCfg := httpclient.DefaultConfig()
	httpCfg.Timeout = 5 * time.Second
	httpClient := httpclient.New(&httpCfg)

	client := NewClient(cfg, httpClient)

	return client, server
}

func TestNewClient(t *testing.T) {
	cfg := &config.FeishuConfig{
		AppID:        "test_app_id",
		AppSecret:    "test_app_secret",
		ApprovalCode: "test_approval_code",
		ChatID:       "test_chat_id",
		BaseURL:      "https://open.feishu.cn",
	}

	httpClient := httpclient.New(nil)
	client := NewClient(cfg, httpClient)

	assert.NotNil(t, client)
	assert.Equal(t, cfg, client.config)
	assert.Equal(t, httpClient, client.httpClient)
	assert.Empty(t, client.token)
	assert.True(t, client.tokenExp.IsZero())
}

func TestGetAccessToken_Success(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		assert.Equal(t, "/open-apis/auth/v3/tenant_access_token/internal", r.URL.Path)
		assert.Equal(t, "POST", r.Method)

		var req tokenRequest
		err := json.NewDecoder(r.Body).Decode(&req)
		require.NoError(t, err)
		assert.Equal(t, "test_app_id", req.AppID)
		assert.Equal(t, "test_app_secret", req.AppSecret)

		resp := tokenResponse{
			Code:              0,
			Msg:               "success",
			TenantAccessToken: "test_token_12345",
			Expire:            7200, // 2 hours
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	token, err := client.GetAccessToken(ctx)

	require.NoError(t, err)
	assert.Equal(t, "test_token_12345", token)
	assert.True(t, client.IsTokenValid())
}

func TestGetAccessToken_Caching(t *testing.T) {
	var callCount int32

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&callCount, 1)

		resp := tokenResponse{
			Code:              0,
			Msg:               "success",
			TenantAccessToken: "cached_token",
			Expire:            7200,
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()

	// First call should fetch the token
	token1, err := client.GetAccessToken(ctx)
	require.NoError(t, err)
	assert.Equal(t, "cached_token", token1)
	assert.Equal(t, int32(1), atomic.LoadInt32(&callCount))

	// Second call should use cached token
	token2, err := client.GetAccessToken(ctx)
	require.NoError(t, err)
	assert.Equal(t, "cached_token", token2)
	assert.Equal(t, int32(1), atomic.LoadInt32(&callCount)) // Still 1, no new request

	// Third call should also use cached token
	token3, err := client.GetAccessToken(ctx)
	require.NoError(t, err)
	assert.Equal(t, "cached_token", token3)
	assert.Equal(t, int32(1), atomic.LoadInt32(&callCount)) // Still 1
}

func TestGetAccessToken_ConcurrentAccess(t *testing.T) {
	var callCount int32

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&callCount, 1)
		// Simulate some processing time
		time.Sleep(50 * time.Millisecond)

		resp := tokenResponse{
			Code:              0,
			Msg:               "success",
			TenantAccessToken: "concurrent_token",
			Expire:            7200,
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	var wg sync.WaitGroup
	numGoroutines := 10

	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			token, err := client.GetAccessToken(ctx)
			assert.NoError(t, err)
			assert.Equal(t, "concurrent_token", token)
		}()
	}

	wg.Wait()

	// Due to the double-check locking pattern, we should have minimal API calls
	// In practice, it might be 1-2 calls depending on timing
	assert.LessOrEqual(t, atomic.LoadInt32(&callCount), int32(3))
}

func TestGetAccessToken_APIError(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := tokenResponse{
			Code: 99991663,
			Msg:  "app_id or app_secret error",
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	_, err := client.GetAccessToken(ctx)

	require.Error(t, err)
	assert.Contains(t, err.Error(), "feishu API error")
	assert.Contains(t, err.Error(), "99991663")
}

func TestGetAccessToken_ContextCancellation(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Simulate slow response
		time.Sleep(2 * time.Second)
		resp := tokenResponse{
			Code:              0,
			TenantAccessToken: "token",
			Expire:            7200,
		}
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	_, err := client.GetAccessToken(ctx)
	require.Error(t, err)
}

func TestInvalidateToken(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := tokenResponse{
			Code:              0,
			TenantAccessToken: "test_token",
			Expire:            7200,
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()

	// Get token first
	_, err := client.GetAccessToken(ctx)
	require.NoError(t, err)
	assert.True(t, client.IsTokenValid())

	// Invalidate token
	client.InvalidateToken()
	assert.False(t, client.IsTokenValid())
}

func TestGetNewInstances_Success(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/open-apis/auth/v3/tenant_access_token/internal" {
			resp := tokenResponse{
				Code:              0,
				TenantAccessToken: "test_token",
				Expire:            7200,
			}
			json.NewEncoder(w).Encode(resp)
			return
		}

		assert.Contains(t, r.URL.Path, "/open-apis/approval/v4/instances")
		assert.Equal(t, "GET", r.Method)
		assert.Contains(t, r.Header.Get("Authorization"), "Bearer test_token")

		resp := instanceListResponse{
			Code: 0,
			Msg:  "success",
			Data: struct {
				InstanceCodeList []string `json:"instance_code_list"`
				PageToken        string   `json:"page_token"`
				HasMore          bool     `json:"has_more"`
			}{
				InstanceCodeList: []string{"INSTANCE-001", "INSTANCE-002", "INSTANCE-003"},
				HasMore:          false,
			},
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	startTime := time.Now().Add(-30 * time.Minute)
	endTime := time.Now()

	instances, err := client.GetNewInstances(ctx, startTime, endTime)

	require.NoError(t, err)
	assert.Len(t, instances, 3)
	assert.Contains(t, instances, "INSTANCE-001")
	assert.Contains(t, instances, "INSTANCE-002")
	assert.Contains(t, instances, "INSTANCE-003")
}

func TestGetNewInstances_EmptyResult(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/open-apis/auth/v3/tenant_access_token/internal" {
			resp := tokenResponse{
				Code:              0,
				TenantAccessToken: "test_token",
				Expire:            7200,
			}
			json.NewEncoder(w).Encode(resp)
			return
		}

		resp := instanceListResponse{
			Code: 0,
			Msg:  "success",
			Data: struct {
				InstanceCodeList []string `json:"instance_code_list"`
				PageToken        string   `json:"page_token"`
				HasMore          bool     `json:"has_more"`
			}{
				InstanceCodeList: []string{},
				HasMore:          false,
			},
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	instances, err := client.GetNewInstances(ctx, time.Now().Add(-1*time.Hour), time.Now())

	require.NoError(t, err)
	assert.Empty(t, instances)
}

func TestGetInstanceDetail_Success(t *testing.T) {
	expectedData := map[string]interface{}{
		"instance_code": "INSTANCE-001",
		"status":        "APPROVED",
		"form":          "test form data",
	}

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/open-apis/auth/v3/tenant_access_token/internal" {
			resp := tokenResponse{
				Code:              0,
				TenantAccessToken: "test_token",
				Expire:            7200,
			}
			json.NewEncoder(w).Encode(resp)
			return
		}

		assert.Equal(t, "/open-apis/approval/v4/instances/INSTANCE-001", r.URL.Path)
		assert.Equal(t, "GET", r.Method)

		resp := map[string]interface{}{
			"code": 0,
			"msg":  "success",
			"data": expectedData,
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	body, err := client.GetInstanceDetail(ctx, "INSTANCE-001")

	require.NoError(t, err)
	assert.NotEmpty(t, body)

	// Verify the response contains expected data
	var resp map[string]interface{}
	err = json.Unmarshal(body, &resp)
	require.NoError(t, err)
	assert.Equal(t, float64(0), resp["code"])
}

func TestSendUserNotification_Success(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/open-apis/auth/v3/tenant_access_token/internal" {
			resp := tokenResponse{
				Code:              0,
				TenantAccessToken: "test_token",
				Expire:            7200,
			}
			json.NewEncoder(w).Encode(resp)
			return
		}

		if r.URL.Path == "/open-apis/contact/v3/users/batch_get_id" {
			resp := userIDResponse{
				Code: 0,
				Msg:  "success",
				Data: struct {
					UserList []struct {
						UserID string `json:"user_id"`
						Mobile string `json:"mobile,omitempty"`
						Email  string `json:"email,omitempty"`
					} `json:"user_list"`
				}{
					UserList: []struct {
						UserID string `json:"user_id"`
						Mobile string `json:"mobile,omitempty"`
						Email  string `json:"email,omitempty"`
					}{
						{UserID: "ou_test_user_id"},
					},
				},
			}
			json.NewEncoder(w).Encode(resp)
			return
		}

		if r.URL.Path == "/open-apis/im/v1/messages" {
			assert.Contains(t, r.URL.RawQuery, "receive_id_type=open_id")

			var reqBody map[string]interface{}
			json.NewDecoder(r.Body).Decode(&reqBody)
			assert.Equal(t, "ou_test_user_id", reqBody["receive_id"])
			assert.Equal(t, "interactive", reqBody["msg_type"])

			resp := APIResponse{Code: 0, Msg: "success"}
			json.NewEncoder(w).Encode(resp)
			return
		}

		t.Errorf("Unexpected request path: %s", r.URL.Path)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	err := client.SendUserNotification(ctx, "test@pingcap.com", "Test Title", "Test Message")

	require.NoError(t, err)
}

func TestSendGroupNotification_Success(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/open-apis/auth/v3/tenant_access_token/internal" {
			resp := tokenResponse{
				Code:              0,
				TenantAccessToken: "test_token",
				Expire:            7200,
			}
			json.NewEncoder(w).Encode(resp)
			return
		}

		if r.URL.Path == "/open-apis/im/v1/messages" {
			assert.Contains(t, r.URL.RawQuery, "receive_id_type=chat_id")

			var reqBody map[string]interface{}
			json.NewDecoder(r.Body).Decode(&reqBody)
			assert.Equal(t, "oc_test_chat_id", reqBody["receive_id"])
			assert.Equal(t, "interactive", reqBody["msg_type"])

			resp := APIResponse{Code: 0, Msg: "success"}
			json.NewEncoder(w).Encode(resp)
			return
		}

		t.Errorf("Unexpected request path: %s", r.URL.Path)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	err := client.SendGroupNotification(ctx, "oc_test_chat_id", "Test Title", "Test Message")

	require.NoError(t, err)
}

func TestSendUserNotification_UserNotFound(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/open-apis/auth/v3/tenant_access_token/internal" {
			resp := tokenResponse{
				Code:              0,
				TenantAccessToken: "test_token",
				Expire:            7200,
			}
			json.NewEncoder(w).Encode(resp)
			return
		}

		if r.URL.Path == "/open-apis/contact/v3/users/batch_get_id" {
			resp := userIDResponse{
				Code: 0,
				Msg:  "success",
				Data: struct {
					UserList []struct {
						UserID string `json:"user_id"`
						Mobile string `json:"mobile,omitempty"`
						Email  string `json:"email,omitempty"`
					} `json:"user_list"`
				}{
					UserList: []struct {
						UserID string `json:"user_id"`
						Mobile string `json:"mobile,omitempty"`
						Email  string `json:"email,omitempty"`
					}{}, // Empty list - user not found
				},
			}
			json.NewEncoder(w).Encode(resp)
			return
		}

		t.Errorf("Unexpected request path: %s", r.URL.Path)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	err := client.SendUserNotification(ctx, "nonexistent@pingcap.com", "Test Title", "Test Message")

	require.Error(t, err)
	assert.Contains(t, err.Error(), "user not found")
}

func TestBuildInteractiveCard(t *testing.T) {
	title := "Test Title"
	message := "Test Message Content"

	cardJSON := buildInteractiveCard(title, message)

	var card map[string]interface{}
	err := json.Unmarshal([]byte(cardJSON), &card)
	require.NoError(t, err)

	// Verify config
	config, ok := card["config"].(map[string]interface{})
	require.True(t, ok)
	assert.Equal(t, true, config["wide_screen_mode"])

	// Verify header
	header, ok := card["header"].(map[string]interface{})
	require.True(t, ok)
	assert.Equal(t, "turquoise", header["template"])

	headerTitle, ok := header["title"].(map[string]interface{})
	require.True(t, ok)
	assert.Equal(t, title, headerTitle["content"])
	assert.Equal(t, "plain_text", headerTitle["tag"])

	// Verify elements
	elements, ok := card["elements"].([]interface{})
	require.True(t, ok)
	require.Len(t, elements, 1)

	element, ok := elements[0].(map[string]interface{})
	require.True(t, ok)
	assert.Equal(t, "div", element["tag"])

	text, ok := element["text"].(map[string]interface{})
	require.True(t, ok)
	assert.Equal(t, message, text["content"])
	assert.Equal(t, "lark_md", text["tag"])
}

func TestGetConfig(t *testing.T) {
	cfg := &config.FeishuConfig{
		AppID:        "test_app_id",
		AppSecret:    "test_app_secret",
		ApprovalCode: "test_approval_code",
		ChatID:       "test_chat_id",
		BaseURL:      "https://open.feishu.cn",
	}

	httpClient := httpclient.New(nil)
	client := NewClient(cfg, httpClient)

	returnedCfg := client.GetConfig()
	assert.Equal(t, cfg, returnedCfg)
	assert.Equal(t, "test_chat_id", returnedCfg.ChatID)
}

func TestTokenExpirySafetyMargin(t *testing.T) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := tokenResponse{
			Code:              0,
			Msg:               "success",
			TenantAccessToken: "test_token",
			Expire:            600, // 10 minutes - safety margin should apply
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	client, server := createTestClient(t, handler)
	defer server.Close()

	ctx := context.Background()
	_, err := client.GetAccessToken(ctx)
	require.NoError(t, err)

	// Token should be valid but with safety margin applied
	assert.True(t, client.IsTokenValid())

	// The expiry should be less than 10 minutes from now (due to 5 min safety margin)
	client.mu.RLock()
	timeUntilExpiry := time.Until(client.tokenExp)
	client.mu.RUnlock()

	assert.Less(t, timeUntilExpiry, 10*time.Minute)
	assert.Greater(t, timeUntilExpiry, 4*time.Minute) // Should be around 5 minutes
}
