package weibo

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/chenhg5/cc-connect/core"
	"github.com/gorilla/websocket"
)

func TestNew_RequiredFields(t *testing.T) {
	tests := []struct {
		name    string
		opts    map[string]any
		wantErr bool
	}{
		{"missing both", map[string]any{}, true},
		{"missing app_secret", map[string]any{"app_id": "id"}, true},
		{"missing app_id", map[string]any{"app_secret": "secret"}, true},
		{"valid", map[string]any{"app_id": "id", "app_secret": "secret"}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p, err := New(tt.opts)
			if tt.wantErr {
				if err == nil {
					t.Error("expected error")
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if p.Name() != "weibo" {
				t.Errorf("name = %q, want %q", p.Name(), "weibo")
			}
		})
	}
}

func TestNew_CustomName(t *testing.T) {
	p, err := New(map[string]any{
		"app_id":     "id",
		"app_secret": "secret",
		"name":       "my-weibo",
	})
	if err != nil {
		t.Fatal(err)
	}
	if p.Name() != "my-weibo" {
		t.Errorf("name = %q, want %q", p.Name(), "my-weibo")
	}
}

func TestNew_CustomEndpoints(t *testing.T) {
	p, err := New(map[string]any{
		"app_id":         "id",
		"app_secret":     "secret",
		"token_endpoint": "https://custom.example.com/token",
		"ws_endpoint":    "ws://custom.example.com/ws",
	})
	if err != nil {
		t.Fatal(err)
	}
	plat := p.(*Platform)
	if plat.tokenEndpoint != "https://custom.example.com/token" {
		t.Errorf("tokenEndpoint = %q", plat.tokenEndpoint)
	}
	if plat.wsEndpoint != "ws://custom.example.com/ws" {
		t.Errorf("wsEndpoint = %q", plat.wsEndpoint)
	}
}

func TestSplitText(t *testing.T) {
	tests := []struct {
		name   string
		text   string
		limit  int
		chunks int
	}{
		{"short", "hello", 100, 1},
		{"exact", "abcde", 5, 1},
		{"split", "abcdefgh", 3, 3},
		{"empty", "", 10, 1},
		{"unicode", "你好世界测试", 3, 2},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := splitText(tt.text, tt.limit)
			if len(result) != tt.chunks {
				t.Errorf("splitText(%q, %d) = %d chunks, want %d", tt.text, tt.limit, len(result), tt.chunks)
			}
			joined := strings.Join(result, "")
			if joined != tt.text {
				t.Errorf("joined = %q, want %q", joined, tt.text)
			}
		})
	}
}

func TestIsDuplicate(t *testing.T) {
	p := &Platform{seen: make(map[string]struct{})}

	if p.isDuplicate("msg1") {
		t.Error("first occurrence should not be duplicate")
	}
	if !p.isDuplicate("msg1") {
		t.Error("second occurrence should be duplicate")
	}
	if p.isDuplicate("msg2") {
		t.Error("different message should not be duplicate")
	}
}

func TestIsDuplicate_Prune(t *testing.T) {
	p := &Platform{seen: make(map[string]struct{})}

	for i := 0; i < maxSeenMessages+100; i++ {
		p.isDuplicate(strings.Repeat("x", 10) + string(rune(i)))
	}
	if len(p.seen) > maxSeenMessages {
		t.Errorf("seen map should be pruned, got %d entries", len(p.seen))
	}
}

func TestHandleInbound(t *testing.T) {
	p := &Platform{
		name:      "weibo",
		allowFrom: "*",
		seen:      make(map[string]struct{}),
	}

	var received *core.Message
	var mu sync.Mutex
	p.handler = func(_ core.Platform, msg *core.Message) {
		mu.Lock()
		received = msg
		mu.Unlock()
	}

	payload := messagePayload{
		MessageID:  "test-123",
		FromUserID: "user1",
		Text:       "hello world",
		Timestamp:  1234567890,
	}
	raw, _ := json.Marshal(payload)
	p.handleInbound(raw)

	mu.Lock()
	defer mu.Unlock()
	if received == nil {
		t.Fatal("handler not called")
	}
	if received.SessionKey != "weibo:user1:user1" {
		t.Errorf("sessionKey = %q", received.SessionKey)
	}
	if received.Content != "hello world" {
		t.Errorf("content = %q", received.Content)
	}
	if received.UserID != "user1" {
		t.Errorf("userID = %q", received.UserID)
	}
	if received.MessageID != "test-123" {
		t.Errorf("messageID = %q", received.MessageID)
	}
}

func TestHandleInbound_AllowList(t *testing.T) {
	p := &Platform{
		name:      "weibo",
		allowFrom: "user2,user3",
		seen:      make(map[string]struct{}),
	}

	called := false
	p.handler = func(_ core.Platform, _ *core.Message) {
		called = true
	}

	payload := messagePayload{
		MessageID:  "blocked-1",
		FromUserID: "user1",
		Text:       "hello",
	}
	raw, _ := json.Marshal(payload)
	p.handleInbound(raw)

	if called {
		t.Error("handler should not be called for unauthorized user")
	}
}

func TestHandleInbound_EmptyText(t *testing.T) {
	p := &Platform{
		name:      "weibo",
		allowFrom: "*",
		seen:      make(map[string]struct{}),
	}

	called := false
	p.handler = func(_ core.Platform, _ *core.Message) {
		called = true
	}

	payload := messagePayload{
		MessageID:  "empty-1",
		FromUserID: "user1",
		Text:       "",
	}
	raw, _ := json.Marshal(payload)
	p.handleInbound(raw)

	if called {
		t.Error("handler should not be called for empty text without attachments")
	}
}

func TestHandleInbound_WithImage(t *testing.T) {
	p := &Platform{
		name:      "weibo",
		allowFrom: "*",
		seen:      make(map[string]struct{}),
	}

	var received *core.Message
	var mu sync.Mutex
	p.handler = func(_ core.Platform, msg *core.Message) {
		mu.Lock()
		received = msg
		mu.Unlock()
	}

	imgData := []byte("fake-png-data")
	b64 := base64.StdEncoding.EncodeToString(imgData)

	payload := messagePayload{
		MessageID:  "img-1",
		FromUserID: "user1",
		Text:       "check this image",
		Input: []messageInputItem{{
			Type: "message",
			Role: "user",
			Content: []contentPart{{
				Type:     "input_image",
				FileName: "photo.png",
				Source:   &inputSource{Type: "base64", MediaType: "image/png", Data: b64},
			}},
		}},
	}
	raw, _ := json.Marshal(payload)
	p.handleInbound(raw)

	mu.Lock()
	defer mu.Unlock()
	if received == nil {
		t.Fatal("handler not called")
	}
	if received.Content != "check this image" {
		t.Errorf("content = %q", received.Content)
	}
	if len(received.Images) != 1 {
		t.Fatalf("images = %d, want 1", len(received.Images))
	}
	if received.Images[0].MimeType != "image/png" {
		t.Errorf("image mime = %q", received.Images[0].MimeType)
	}
	if received.Images[0].FileName != "photo.png" {
		t.Errorf("image filename = %q", received.Images[0].FileName)
	}
	if string(received.Images[0].Data) != "fake-png-data" {
		t.Errorf("image data mismatch")
	}
}

func TestHandleInbound_WithFile(t *testing.T) {
	p := &Platform{
		name:      "weibo",
		allowFrom: "*",
		seen:      make(map[string]struct{}),
	}

	var received *core.Message
	var mu sync.Mutex
	p.handler = func(_ core.Platform, msg *core.Message) {
		mu.Lock()
		received = msg
		mu.Unlock()
	}

	fileData := []byte("hello world pdf content")
	b64 := base64.StdEncoding.EncodeToString(fileData)

	payload := messagePayload{
		MessageID:  "file-1",
		FromUserID: "user1",
		Text:       "",
		Input: []messageInputItem{{
			Type: "message",
			Role: "user",
			Content: []contentPart{
				{Type: "input_text", Text: "here is my file"},
				{
					Type:     "input_file",
					FileName: "doc.pdf",
					Source:   &inputSource{Type: "base64", MediaType: "application/pdf", Data: b64},
				},
			},
		}},
	}
	raw, _ := json.Marshal(payload)
	p.handleInbound(raw)

	mu.Lock()
	defer mu.Unlock()
	if received == nil {
		t.Fatal("handler not called")
	}
	if received.Content != "here is my file" {
		t.Errorf("content = %q, want %q", received.Content, "here is my file")
	}
	if len(received.Files) != 1 {
		t.Fatalf("files = %d, want 1", len(received.Files))
	}
	if received.Files[0].MimeType != "application/pdf" {
		t.Errorf("file mime = %q", received.Files[0].MimeType)
	}
	if received.Files[0].FileName != "doc.pdf" {
		t.Errorf("file name = %q", received.Files[0].FileName)
	}
}

func TestHandleInbound_ImageOnlyNoText(t *testing.T) {
	p := &Platform{
		name:      "weibo",
		allowFrom: "*",
		seen:      make(map[string]struct{}),
	}

	var received *core.Message
	p.handler = func(_ core.Platform, msg *core.Message) {
		received = msg
	}

	imgData := []byte("image-bytes")
	b64 := base64.StdEncoding.EncodeToString(imgData)

	payload := messagePayload{
		MessageID:  "imgonly-1",
		FromUserID: "user1",
		Text:       "",
		Input: []messageInputItem{{
			Type: "message",
			Role: "user",
			Content: []contentPart{{
				Type:   "input_image",
				Source: &inputSource{Type: "base64", MediaType: "image/jpeg", Data: b64},
			}},
		}},
	}
	raw, _ := json.Marshal(payload)
	p.handleInbound(raw)

	if received == nil {
		t.Fatal("handler should be called for image-only message")
	}
	if len(received.Images) != 1 {
		t.Errorf("images = %d, want 1", len(received.Images))
	}
}

func TestHandleInbound_UnsupportedImageMime(t *testing.T) {
	p := &Platform{
		name:      "weibo",
		allowFrom: "*",
		seen:      make(map[string]struct{}),
	}

	var received *core.Message
	p.handler = func(_ core.Platform, msg *core.Message) {
		received = msg
	}

	b64 := base64.StdEncoding.EncodeToString([]byte("bmp-data"))

	payload := messagePayload{
		MessageID:  "bmp-1",
		FromUserID: "user1",
		Text:       "a bmp image",
		Input: []messageInputItem{{
			Type: "message",
			Role: "user",
			Content: []contentPart{{
				Type:   "input_image",
				Source: &inputSource{Type: "base64", MediaType: "image/bmp", Data: b64},
			}},
		}},
	}
	raw, _ := json.Marshal(payload)
	p.handleInbound(raw)

	if received == nil {
		t.Fatal("handler should be called for text content")
	}
	if len(received.Images) != 0 {
		t.Errorf("unsupported image should be filtered, got %d images", len(received.Images))
	}
}

func TestHandleInbound_InputTextOverridesPayloadText(t *testing.T) {
	p := &Platform{
		name:      "weibo",
		allowFrom: "*",
		seen:      make(map[string]struct{}),
	}

	var received *core.Message
	p.handler = func(_ core.Platform, msg *core.Message) {
		received = msg
	}

	payload := messagePayload{
		MessageID:  "override-1",
		FromUserID: "user1",
		Text:       "payload text",
		Input: []messageInputItem{{
			Type: "message",
			Role: "user",
			Content: []contentPart{
				{Type: "input_text", Text: "input part 1"},
				{Type: "input_text", Text: "input part 2"},
			},
		}},
	}
	raw, _ := json.Marshal(payload)
	p.handleInbound(raw)

	if received == nil {
		t.Fatal("handler not called")
	}
	if received.Content != "input part 1\ninput part 2" {
		t.Errorf("content = %q, want joined input_text", received.Content)
	}
}

func TestNormalizeInboundInput_SkipsNonUserRole(t *testing.T) {
	payload := messagePayload{
		FromUserID: "user1",
		Text:       "fallback",
		Input: []messageInputItem{
			{
				Type: "message",
				Role: "assistant",
				Content: []contentPart{
					{Type: "input_text", Text: "should be ignored"},
				},
			},
		},
	}

	text, images, files := normalizeInboundInput(payload)
	if text != "fallback" {
		t.Errorf("text = %q, want fallback", text)
	}
	if len(images) != 0 || len(files) != 0 {
		t.Error("should have no attachments from non-user role")
	}
}

func TestRefreshToken(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "POST" {
			t.Errorf("method = %s, want POST", r.Method)
		}
		json.NewEncoder(w).Encode(map[string]any{
			"data": map[string]any{
				"token":     "test-token-abc",
				"expire_in": 3600,
				"uid":       12345,
			},
		})
	}))
	defer ts.Close()

	p := &Platform{
		appID:         "test-app",
		appSecret:     "test-secret",
		tokenEndpoint: ts.URL,
		seen:          make(map[string]struct{}),
	}

	tok, err := p.refreshToken()
	if err != nil {
		t.Fatal(err)
	}
	if tok != "test-token-abc" {
		t.Errorf("token = %q, want %q", tok, "test-token-abc")
	}
	if p.uid != "12345" {
		t.Errorf("uid = %q, want %q", p.uid, "12345")
	}
}

func TestSendMessage(t *testing.T) {
	upgrader := websocket.Upgrader{}
	gotMsg := make(chan map[string]any, 1)

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		c, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			return
		}
		defer c.Close()
		for {
			_, msg, err := c.ReadMessage()
			if err != nil {
				return
			}
			var m map[string]any
			json.Unmarshal(msg, &m)
			gotMsg <- m
		}
	}))
	defer ts.Close()

	wsURL := "ws" + strings.TrimPrefix(ts.URL, "http")
	ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err != nil {
		t.Fatal(err)
	}
	defer ws.Close()

	p := &Platform{
		name: "weibo",
		ws:   ws,
		seen: make(map[string]struct{}),
	}

	rctx := replyContext{fromUserID: "user1", sessionKey: "weibo:user1:user1"}
	err = p.sendMessage(rctx, "short message")
	if err != nil {
		t.Fatal(err)
	}

	select {
	case m := <-gotMsg:
		if m["type"] != "send_message" {
			t.Errorf("type = %v", m["type"])
		}
		payload := m["payload"].(map[string]any)
		if payload["toUserId"] != "user1" {
			t.Errorf("toUserId = %v", payload["toUserId"])
		}
		if payload["text"] != "short message" {
			t.Errorf("text = %v", payload["text"])
		}
		if payload["done"] != true {
			t.Errorf("done = %v", payload["done"])
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for message")
	}
}

func newWSTestPlatform(t *testing.T) (*Platform, chan map[string]any) {
	t.Helper()
	upgrader := websocket.Upgrader{}
	gotMsg := make(chan map[string]any, 5)

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		c, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			return
		}
		defer c.Close()
		for {
			_, msg, err := c.ReadMessage()
			if err != nil {
				return
			}
			var m map[string]any
			json.Unmarshal(msg, &m)
			gotMsg <- m
		}
	}))
	t.Cleanup(ts.Close)

	wsURL := "ws" + strings.TrimPrefix(ts.URL, "http")
	ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ws.Close() })

	p := &Platform{name: "weibo", ws: ws, seen: make(map[string]struct{})}
	return p, gotMsg
}

func TestSendImage(t *testing.T) {
	p, gotMsg := newWSTestPlatform(t)

	rctx := replyContext{fromUserID: "user1", sessionKey: "weibo:user1:user1"}
	imgData := []byte("fake-image-bytes")

	err := p.SendImage(context.Background(), rctx, core.ImageAttachment{
		MimeType: "image/png",
		Data:     imgData,
		FileName: "screenshot.png",
	})
	if err != nil {
		t.Fatal(err)
	}

	select {
	case m := <-gotMsg:
		if m["type"] != "send_message" {
			t.Errorf("type = %v", m["type"])
		}
		payload := m["payload"].(map[string]any)
		if payload["toUserId"] != "user1" {
			t.Errorf("toUserId = %v", payload["toUserId"])
		}
		if payload["done"] != true {
			t.Errorf("done = %v", payload["done"])
		}

		input, ok := payload["input"].([]any)
		if !ok || len(input) == 0 {
			t.Fatal("input missing or empty")
		}
		item := input[0].(map[string]any)
		if item["role"] != "assistant" {
			t.Errorf("role = %v", item["role"])
		}
		content := item["content"].([]any)
		part := content[0].(map[string]any)
		if part["type"] != "input_image" {
			t.Errorf("part type = %v", part["type"])
		}
		if part["filename"] != "screenshot.png" {
			t.Errorf("filename = %v", part["filename"])
		}
		src := part["source"].(map[string]any)
		if src["media_type"] != "image/png" {
			t.Errorf("media_type = %v", src["media_type"])
		}
		decoded, _ := base64.StdEncoding.DecodeString(src["data"].(string))
		if string(decoded) != string(imgData) {
			t.Error("image data mismatch after round-trip")
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out")
	}
}

func TestSendFile(t *testing.T) {
	p, gotMsg := newWSTestPlatform(t)

	rctx := replyContext{fromUserID: "user1", sessionKey: "weibo:user1:user1"}
	fileData := []byte("pdf-content-here")

	err := p.SendFile(context.Background(), rctx, core.FileAttachment{
		MimeType: "application/pdf",
		Data:     fileData,
		FileName: "report.pdf",
	})
	if err != nil {
		t.Fatal(err)
	}

	select {
	case m := <-gotMsg:
		payload := m["payload"].(map[string]any)
		input := payload["input"].([]any)
		item := input[0].(map[string]any)
		content := item["content"].([]any)
		part := content[0].(map[string]any)
		if part["type"] != "input_file" {
			t.Errorf("part type = %v", part["type"])
		}
		if part["filename"] != "report.pdf" {
			t.Errorf("filename = %v", part["filename"])
		}
		src := part["source"].(map[string]any)
		if src["media_type"] != "application/pdf" {
			t.Errorf("media_type = %v", src["media_type"])
		}
		decoded, _ := base64.StdEncoding.DecodeString(src["data"].(string))
		if string(decoded) != string(fileData) {
			t.Error("file data mismatch")
		}
	case <-time.After(2 * time.Second):
		t.Fatal("timed out")
	}
}

func TestSendImage_NotConnected(t *testing.T) {
	p := &Platform{name: "weibo", seen: make(map[string]struct{})}
	rctx := replyContext{fromUserID: "u1"}

	err := p.SendImage(context.Background(), rctx, core.ImageAttachment{Data: []byte("x")})
	if err == nil {
		t.Error("expected error when not connected")
	}
	if !strings.Contains(err.Error(), "not connected") {
		t.Errorf("error = %q, want 'not connected'", err.Error())
	}
}

func TestSendFile_InvalidContext(t *testing.T) {
	p := &Platform{name: "weibo", seen: make(map[string]struct{})}
	err := p.SendFile(context.Background(), "invalid", core.FileAttachment{Data: []byte("x")})
	if err == nil {
		t.Error("expected error for invalid context")
	}
}

func TestInterfaceCompliance(t *testing.T) {
	var _ core.ImageSender = (*Platform)(nil)
	var _ core.FileSender = (*Platform)(nil)
}

// TestWriteWS_ConcurrentSendsSerialized verifies that writeWS serializes
// concurrent callers as gorilla/websocket requires (one writer at a time).
// Without the wsMu fix, parallel WriteJSON calls race on the underlying
// Conn.writer field (caught by go test -race) and may interleave frames.
func TestWriteWS_ConcurrentSendsSerialized(t *testing.T) {
	upgrader := websocket.Upgrader{}
	const n = 50
	gotMsg := make(chan map[string]any, n*2)

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		c, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			return
		}
		defer c.Close()
		for {
			_, msg, err := c.ReadMessage()
			if err != nil {
				return
			}
			var m map[string]any
			if err := json.Unmarshal(msg, &m); err != nil {
				gotMsg <- map[string]any{"_parse_error": err.Error(), "_raw": string(msg)}
				continue
			}
			gotMsg <- m
		}
	}))
	defer ts.Close()

	wsURL := "ws" + strings.TrimPrefix(ts.URL, "http")
	ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err != nil {
		t.Fatal(err)
	}
	defer ws.Close()

	p := &Platform{name: "weibo", ws: ws, seen: make(map[string]struct{})}

	var wg sync.WaitGroup
	errs := make(chan error, n)
	for i := 0; i < n; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			rctx := replyContext{fromUserID: fmt.Sprintf("u%d", i)}
			if err := p.sendMessage(rctx, fmt.Sprintf("m%d", i)); err != nil {
				errs <- err
			}
		}(i)
	}
	wg.Wait()
	close(errs)

	for err := range errs {
		t.Errorf("concurrent sendMessage: %v", err)
	}

	seen := map[string]bool{}
	deadline := time.After(3 * time.Second)
	for len(seen) < n {
		select {
		case m := <-gotMsg:
			if pe, ok := m["_parse_error"]; ok {
				t.Fatalf("server got malformed JSON frame (concurrent write interleaved): %v raw=%q", pe, m["_raw"])
			}
			payload, ok := m["payload"].(map[string]any)
			if !ok {
				t.Fatalf("frame missing payload: %v", m)
			}
			to, _ := payload["toUserId"].(string)
			if to == "" {
				t.Fatalf("frame missing toUserId: %v", payload)
			}
			if seen[to] {
				t.Fatalf("duplicate frame for %s", to)
			}
			seen[to] = true
		case <-deadline:
			t.Fatalf("only got %d of %d messages within 3s", len(seen), n)
		}
	}
	if len(seen) != n {
		t.Fatalf("got %d unique frames, want %d", len(seen), n)
	}
}
