package ws

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

	"github.com/gorilla/websocket"

	"hakemsho/internal/game"
)

// anyMsg تمام فیلدهای ممکن پیام‌های سرور را برای تست جمع می‌کند.
type anyMsg struct {
	Type     string       `json:"type"`
	Seat     int          `json:"seat"`
	YourSeat int          `json:"your_seat"`
	Phase    string       `json:"phase"`
	Hakem    int          `json:"hakem"`
	Turn     int          `json:"turn"`
	YourHand  []string     `json:"your_hand"`
	LeadSuit  string       `json:"lead_suit"`
	TrickDone bool         `json:"trick_done"`
	Players   []PlayerInfo `json:"players"`
	Code      string       `json:"code"` // table_lobby
	Host      bool         `json:"host"` // table_lobby
	Text      string       `json:"text"`  // chat
	Emoji     string       `json:"emoji"` // chat
}

// TestFourPlayersFullGame یک بازی کامل را روی WebSocket واقعی با ۴ ربات اجرا می‌کند.
func TestFourPlayersFullGame(t *testing.T) {
	hub := testHub() // مهلت‌های کوتاه (شامل trickHold) برای سرعت تست
	srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")

	var wg sync.WaitGroup

	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			conn, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(idx), nil)
			if err != nil {
				t.Errorf("dial %d: %v", idx, err)
				return
			}
			defer conn.Close()

			// ورود به صف
			if err := conn.WriteJSON(map[string]string{"type": "join_queue", "mode": "normal"}); err != nil {
				t.Errorf("join %d: %v", idx, err)
				return
			}

			seat := -1
			for {
				_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
				_, raw, err := conn.ReadMessage()
				if err != nil {
					t.Errorf("read %d (seat %d): %v", idx, seat, err)
					return
				}
				var m anyMsg
				if err := json.Unmarshal(raw, &m); err != nil {
					t.Errorf("unmarshal %d: %v", idx, err)
					return
				}
				switch m.Type {
				case "matched":
					seat = m.Seat
				case "state":
					act(conn, &m)
				case "game_over":
					return
				case "error":
					t.Errorf("client %d got error: %s", idx, raw)
				}
			}
		}(i)
	}

	done := make(chan struct{})
	go func() { wg.Wait(); close(done) }()
	select {
	case <-done:
	case <-time.After(25 * time.Second):
		t.Fatal("game did not finish in time")
	}
}

// act ربات سمت‌کلاینت: حکم می‌زند یا کارت مجاز بازی می‌کند.
// در فاز انتخاب حکم، حاکم دقیقاً یک state دریافت می‌کند پس یک‌بار حکم می‌زند (هر هَند).
func act(conn *websocket.Conn, m *anyMsg) {
	switch m.Phase {
	case "choose_trump":
		if m.YourSeat == m.Hakem && len(m.YourHand) > 0 {
			c, _ := game.ParseCard(m.YourHand[0])
			_ = conn.WriteJSON(map[string]string{"type": "choose_trump", "suit": c.Suit.String()})
		}
	case "playing":
		if m.TrickDone || m.Turn != m.YourSeat {
			return
		}
		card := legalCard(m.YourHand, m.LeadSuit)
		_ = conn.WriteJSON(map[string]string{"type": "play_card", "card": card})
	}
}

// testHub یک هاب با مهلت‌های کوتاه برای تست‌ها می‌سازد.
func testHub() *Hub {
	hub := NewHub(func(token string) (int64, string, error) {
		id, err := strconv.ParseInt(token, 10, 64)
		return id, "p" + token, err
	})
	hub.turnTimeout = 8 * time.Millisecond
	hub.botDelay = 8 * time.Millisecond
	hub.matchWait = 300 * time.Millisecond
	hub.trickHold = 5 * time.Millisecond
	hub.firstHandDelay = 0
	hub.dealDelay = 0
	hub.countdownDelay = 20 * time.Millisecond
	go hub.Run()
	return hub
}

// TestPrivateTableFlow: میزبان میز خصوصی می‌سازد، بازیکن دوم می‌پیوندد،
// میزبان شروع می‌کند و بازی (با بات برای ۲ جای خالی) تا پایان پیش می‌رود.
func TestPrivateTableFlow(t *testing.T) {
	hub := testHub()
	srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")

	dial := func(tok string) *websocket.Conn {
		c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+tok, nil)
		if err != nil {
			t.Fatalf("dial %s: %v", tok, err)
		}
		return c
	}

	host := dial("1")
	defer host.Close()
	if err := host.WriteJSON(map[string]string{"type": "create_table"}); err != nil {
		t.Fatal(err)
	}

	// میزبان باید table_lobby با کد دریافت کند.
	code := ""
	for code == "" {
		_ = host.SetReadDeadline(time.Now().Add(2 * time.Second))
		_, raw, err := host.ReadMessage()
		if err != nil {
			t.Fatalf("host read: %v", err)
		}
		var m anyMsg
		_ = json.Unmarshal(raw, &m)
		if m.Type == "table_lobby" {
			if !m.Host {
				t.Fatal("host flag must be true for creator")
			}
			code = m.Code
		}
	}
	if len(code) != 5 {
		t.Fatalf("expected 5-digit code, got %q", code)
	}

	joiner := dial("2")
	defer joiner.Close()
	if err := joiner.WriteJSON(map[string]string{"type": "join_table", "code": code}); err != nil {
		t.Fatal(err)
	}

	// میزبان منتظر می‌ماند تا میز ۲ نفره شود، سپس شروع می‌کند.
	var gotCountdown atomic.Bool
	play := func(conn *websocket.Conn, m *anyMsg) {
		if m.Type == "state" {
			act(conn, m)
		}
	}

	done := make(chan string, 2)
	run := func(conn *websocket.Conn, isHost bool) {
		started := false
		for {
			_ = conn.SetReadDeadline(time.Now().Add(6 * time.Second))
			_, raw, err := conn.ReadMessage()
			if err != nil {
				done <- "read err: " + err.Error()
				return
			}
			var m anyMsg
			_ = json.Unmarshal(raw, &m)
			switch m.Type {
			case "table_lobby":
				if isHost && !started && len(m.Players) >= 2 {
					started = true
					_ = conn.WriteJSON(map[string]string{"type": "start_table"})
				}
			case "countdown":
				gotCountdown.Store(true)
			case "state":
				play(conn, &m)
			case "game_over":
				done <- "ok"
				return
			case "error":
				done <- "error: " + string(raw)
				return
			}
		}
	}
	go run(host, true)
	go run(joiner, false)

	for i := 0; i < 2; i++ {
		select {
		case res := <-done:
			if res != "ok" {
				t.Fatalf("client finished with: %s", res)
			}
		case <-time.After(25 * time.Second):
			t.Fatal("private game did not finish in time")
		}
	}
	if !gotCountdown.Load() {
		t.Fatal("expected countdown message before game start")
	}
}

// TestDisconnectMidGameNoPanic: قطع بازیکن وسط بازی باید بدون panic، رویداد
// player_disconnected تولید کند و بازی با حرکت خودکار ادامه یابد.
func TestDisconnectMidGameNoPanic(t *testing.T) {
	hub := testHub()
	srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")

	conns := make([]*websocket.Conn, 4)
	for i := 0; i < 4; i++ {
		c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i), nil)
		if err != nil {
			t.Fatalf("dial %d: %v", i, err)
		}
		conns[i] = c
		if err := c.WriteJSON(map[string]string{"type": "join_queue"}); err != nil {
			t.Fatalf("join %d: %v", i, err)
		}
	}
	time.Sleep(200 * time.Millisecond)
	conns[0].Close()

	gotDisconnect := false
	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		_ = conns[1].SetReadDeadline(time.Now().Add(2 * time.Second))
		_, raw, err := conns[1].ReadMessage()
		if err != nil {
			break
		}
		var m anyMsg
		_ = json.Unmarshal(raw, &m)
		if m.Type == "player_disconnected" {
			gotDisconnect = true
			break
		}
	}
	for _, c := range conns[1:] {
		c.Close()
	}
	if !gotDisconnect {
		t.Fatal("expected player_disconnected after a player disconnected")
	}
}

// TestBotFill: اگر کمتر از ۴ انسان در صف بماند، میز با بات کامل و بازی شروع می‌شود.
func TestBotFill(t *testing.T) {
	hub := testHub()
	srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")

	// فقط ۲ انسان وصل می‌شوند.
	conns := make([]*websocket.Conn, 2)
	for i := 0; i < 2; i++ {
		c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i), nil)
		if err != nil {
			t.Fatalf("dial %d: %v", i, err)
		}
		defer c.Close()
		conns[i] = c
		_ = c.WriteJSON(map[string]string{"type": "join_queue"})
	}

	// بازیکن ۰ باید matched با ۴ بازیکن (۲ بات) دریافت کند.
	_ = conns[0].SetReadDeadline(time.Now().Add(3 * time.Second))
	bots := -1
	for {
		_, raw, err := conns[0].ReadMessage()
		if err != nil {
			t.Fatalf("read matched: %v", err)
		}
		var m anyMsg
		_ = json.Unmarshal(raw, &m)
		if m.Type == "matched" {
			if len(m.Players) != 4 {
				t.Fatalf("expected 4 players, got %d", len(m.Players))
			}
			bots = 0
			for _, p := range m.Players {
				if p.Bot {
					bots++
				}
			}
			break
		}
	}
	if bots != 2 {
		t.Fatalf("expected 2 bots, got %d", bots)
	}
}

// TestReconnect: بازیکن قطع‌شده می‌تواند با همان توکن به جایگاه قبلی بازگردد.
func TestReconnect(t *testing.T) {
	hub := testHub()
	srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")

	conns := make([]*websocket.Conn, 4)
	for i := 0; i < 4; i++ {
		c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i), nil)
		if err != nil {
			t.Fatalf("dial %d: %v", i, err)
		}
		conns[i] = c
		_ = c.WriteJSON(map[string]string{"type": "join_queue"})
	}
	time.Sleep(150 * time.Millisecond)

	// بازیکن ۰ قطع و سپس با همان توکن دوباره وصل می‌شود.
	conns[0].Close()
	time.Sleep(100 * time.Millisecond)
	rc, _, err := websocket.DefaultDialer.Dial(wsURL+"?token=0", nil)
	if err != nil {
		t.Fatalf("reconnect dial: %v", err)
	}
	defer rc.Close()

	// باید matched با seat=0 و سپس state دریافت کند.
	gotMatched, gotState := false, false
	_ = rc.SetReadDeadline(time.Now().Add(3 * time.Second))
	for !gotState {
		_, raw, err := rc.ReadMessage()
		if err != nil {
			t.Fatalf("read after reconnect: %v", err)
		}
		var m anyMsg
		_ = json.Unmarshal(raw, &m)
		switch m.Type {
		case "matched":
			if m.Seat != 0 {
				t.Fatalf("reconnect to wrong seat: %d", m.Seat)
			}
			gotMatched = true
		case "state":
			gotState = true
		}
	}
	if !gotMatched || !gotState {
		t.Fatal("reconnected client did not receive matched+state")
	}
	for _, c := range conns[1:] {
		c.Close()
	}
}

// TestTurnTimeoutAutoPlays: حتی اگر هیچ بازیکنی حرکت نکند، تایمر نوبت کل بازی
// را خودکار پیش می‌برد تا game_over (مسیر مشترک با حرکت بات/قطع‌شده).
func TestTurnTimeoutAutoPlays(t *testing.T) {
	hub := testHub()
	srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")

	var wg sync.WaitGroup
	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			conn, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(idx), nil)
			if err != nil {
				t.Errorf("dial %d: %v", idx, err)
				return
			}
			defer conn.Close()
			_ = conn.WriteJSON(map[string]string{"type": "join_queue"})
			// عمداً هیچ حرکتی نمی‌زنیم؛ فقط منتظر game_over می‌مانیم.
			for {
				_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
				_, raw, err := conn.ReadMessage()
				if err != nil {
					t.Errorf("client %d read: %v", idx, err)
					return
				}
				var m anyMsg
				_ = json.Unmarshal(raw, &m)
				if m.Type == "game_over" {
					return
				}
			}
		}(i)
	}

	done := make(chan struct{})
	go func() { wg.Wait(); close(done) }()
	select {
	case <-done:
	case <-time.After(20 * time.Second):
		t.Fatal("turn-timeout auto-play did not finish the game")
	}
}

// recordingSettler فراخوانی‌های تسویه را برای بررسی در تست می‌شمارد.
type recordingSettler struct {
	mu       sync.Mutex
	charges  int
	refunds  int
	awards   int
	records  int
	hands    int
	cuts     int
	results  int
}

func (s *recordingSettler) ChargeEntry(int64, string) error {
	s.mu.Lock()
	s.charges++
	s.mu.Unlock()
	return nil
}
func (s *recordingSettler) Refund(int64, string) {
	s.mu.Lock()
	s.refunds++
	s.mu.Unlock()
}
func (s *recordingSettler) AwardWinner(int64, string) {
	s.mu.Lock()
	s.awards++
	s.mu.Unlock()
}
func (s *recordingSettler) AwardRank(int64, string, bool) {}
func (s *recordingSettler) RecordGame(string, string, int, bool) {
	s.mu.Lock()
	s.records++
	s.mu.Unlock()
}

func (s *recordingSettler) RecordHand(int64, bool, bool, bool) {
	s.mu.Lock()
	s.hands++
	s.mu.Unlock()
}
func (s *recordingSettler) RecordCut(int64) {
	s.mu.Lock()
	s.cuts++
	s.mu.Unlock()
}
func (s *recordingSettler) RecordGameResult(int64, bool) {
	s.mu.Lock()
	s.results++
	s.mu.Unlock()
}

func (s *recordingSettler) PlayerProfile(int64) (int64, string, string) { return 0, "", "" }
func (s *recordingSettler) ChargeTicket(int64) error            { return nil }
func (s *recordingSettler) ChargeCoins(int64, int64, string) error { return nil }
func (s *recordingSettler) AwardCoins(int64, int64, string)        {}
func (s *recordingSettler) ChargePrivateTable(int64) error      { return nil }
func (s *recordingSettler) PrivateTableInfo(int64) (int, bool) { return 5, false }

// برای سرعتِ تست، بازی پس از ۲ هَند تمام می‌شود.
func (s *recordingSettler) TargetHands(string) int { return 2 }
func (s *recordingSettler) snapshot() (c, r, a, rec int) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.charges, s.refunds, s.awards, s.records
}

// TestSettlementOnGameOver: ۴ انسانِ بیکار ⇒ ۴ کسر ورودی، بازی خودکار تا پایان،
// ۲ جایزه‌ی برنده و یک ثبت بازی، بدون بازگشت.
func TestSettlementOnGameOver(t *testing.T) {
	hub := testHub()
	settler := &recordingSettler{}
	hub.SetSettler(settler)
	srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")

	var wg sync.WaitGroup
	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			conn, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(idx+1), nil)
			if err != nil {
				t.Errorf("dial %d: %v", idx, err)
				return
			}
			defer conn.Close()
			_ = conn.WriteJSON(map[string]string{"type": "join_queue", "tier": "beginner"})
			for {
				_ = conn.SetReadDeadline(time.Now().Add(6 * time.Second))
				_, raw, err := conn.ReadMessage()
				if err != nil {
					return
				}
				var m anyMsg
				_ = json.Unmarshal(raw, &m)
				if m.Type == "game_over" {
					return
				}
			}
		}(i)
	}
	done := make(chan struct{})
	go func() { wg.Wait(); close(done) }()
	select {
	case <-done:
	case <-time.After(20 * time.Second):
		t.Fatal("game did not finish")
	}
	time.Sleep(100 * time.Millisecond) // فرصت پردازش settle در goroutine میز

	c, r, a, rec := settler.snapshot()
	if c != 4 {
		t.Errorf("charges = %d, want 4", c)
	}
	if a != 2 {
		t.Errorf("awards = %d, want 2 (winning team humans)", a)
	}
	if rec != 1 {
		t.Errorf("records = %d, want 1", rec)
	}
	if r != 0 {
		t.Errorf("refunds = %d, want 0 on normal finish", r)
	}
}

// TestRefundOnAbort: اگر همه‌ی انسان‌ها وسط بازی قطع شوند، ورودی بازگردانده می‌شود.
func TestRefundOnAbort(t *testing.T) {
	hub := testHub()
	settler := &recordingSettler{}
	hub.SetSettler(settler)
	srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")

	conns := make([]*websocket.Conn, 4)
	for i := 0; i < 4; i++ {
		c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i+1), nil)
		if err != nil {
			t.Fatalf("dial %d: %v", i, err)
		}
		conns[i] = c
		_ = c.WriteJSON(map[string]string{"type": "join_queue", "tier": "beginner"})
	}
	time.Sleep(150 * time.Millisecond) // میز تشکیل شود
	for _, c := range conns {
		c.Close()
	}
	time.Sleep(400 * time.Millisecond) // قطع همه ⇒ abort + refund

	charges, refunds, _, _ := settler.snapshot()
	if charges != 4 {
		t.Errorf("charges = %d, want 4", charges)
	}
	if refunds != 4 {
		t.Errorf("refunds = %d, want 4 on abort", refunds)
	}
}

// TestChatBroadcast: پیامِ چتِ یک بازیکن باید برای همه‌ی میز (شاملِ گیرنده‌ی دیگر)
// با جایگاهِ فرستنده و متن پخش شود.
func TestChatBroadcast(t *testing.T) {
	hub := testHub()
	srv := httptest.NewServer(http.HandlerFunc(hub.ServeWS))
	defer srv.Close()
	wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")

	conns := make([]*websocket.Conn, 4)
	seats := make([]int, 4)
	for i := 0; i < 4; i++ {
		c, _, err := websocket.DefaultDialer.Dial(wsURL+"?token="+strconv.Itoa(i), nil)
		if err != nil {
			t.Fatalf("dial %d: %v", i, err)
		}
		defer c.Close()
		conns[i] = c
		_ = c.WriteJSON(map[string]string{"type": "join_queue"})
	}

	// منتظرِ matched برای دانستنِ جایگاهِ فرستنده.
	for i := 0; i < 4; i++ {
		_ = conns[i].SetReadDeadline(time.Now().Add(3 * time.Second))
		for {
			_, raw, err := conns[i].ReadMessage()
			if err != nil {
				t.Fatalf("read matched %d: %v", i, err)
			}
			var m anyMsg
			_ = json.Unmarshal(raw, &m)
			if m.Type == "matched" {
				seats[i] = m.Seat
				break
			}
		}
	}

	// فرستنده: بازیکن ۰ یک پیام می‌فرستد.
	_ = conns[0].WriteJSON(map[string]string{"type": "chat", "text": "سلام"})

	// گیرنده: بازیکن ۱ باید پیامِ chat با جایگاهِ بازیکن ۰ و متنِ درست بگیرد.
	_ = conns[1].SetReadDeadline(time.Now().Add(3 * time.Second))
	got := false
	for !got {
		_, raw, err := conns[1].ReadMessage()
		if err != nil {
			t.Fatalf("read chat: %v", err)
		}
		var m anyMsg
		_ = json.Unmarshal(raw, &m)
		if m.Type == "chat" {
			if m.Seat != seats[0] {
				t.Fatalf("chat seat = %d, want %d", m.Seat, seats[0])
			}
			if m.Text != "سلام" {
				t.Fatalf("chat text = %q, want سلام", m.Text)
			}
			got = true
		}
	}
}

// legalCard یک کارت مجاز انتخاب می‌کند (follow suit در صورت امکان).
func legalCard(hand []string, leadSuit string) string {
	if leadSuit != "" {
		for _, cs := range hand {
			c, err := game.ParseCard(cs)
			if err == nil && c.Suit.String() == leadSuit {
				return cs
			}
		}
	}
	return hand[0]
}
