package bot

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

	"github.com/go-telegram/bot/models"
	"go.uber.org/zap"
)

const (
	testChatID = 555
	botSelfID  = 111111 // the bot's own id in tests (for the loop guard)
)

// chatAPI is a fake Bot API for the chat-gating tests: it answers getMe, returns a
// scripted getChatMember status, and records restrictChatMember and unpinChatMessage calls.
type chatAPI struct {
	memberStatus string // the status getChatMember reports (default "left")
	restricts    []restrictCall
	unpins       []unpinCall
}

type restrictCall struct {
	userID  string
	canSend bool // can_send_messages in the applied permissions
}

// unpinCall records a single unpinChatMessage request (raw form values, mirroring the
// restrictCall style).
type unpinCall struct {
	chatID    string
	messageID string
}

func (a *chatAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch {
	case strings.HasSuffix(r.URL.Path, "/getMe"):
		io.WriteString(w, `{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"t","username":"tb"}}`)
	case strings.HasSuffix(r.URL.Path, "/getChatMember"):
		status := a.memberStatus
		if status == "" {
			status = "left"
		}
		io.WriteString(w, `{"ok":true,"result":{"status":"`+status+`","user":{"id":`+r.FormValue("user_id")+`,"is_bot":false,"first_name":"u"}}}`)
	case strings.HasSuffix(r.URL.Path, "/restrictChatMember"):
		var perms struct {
			CanSendMessages bool `json:"can_send_messages"`
		}
		_ = json.Unmarshal([]byte(r.FormValue("permissions")), &perms)
		a.restricts = append(a.restricts, restrictCall{userID: r.FormValue("user_id"), canSend: perms.CanSendMessages})
		io.WriteString(w, `{"ok":true,"result":true}`)
	case strings.HasSuffix(r.URL.Path, "/unpinChatMessage"):
		a.unpins = append(a.unpins, unpinCall{chatID: r.FormValue("chat_id"), messageID: r.FormValue("message_id")})
		io.WriteString(w, `{"ok":true,"result":true}`)
	default:
		io.WriteString(w, `{"ok":true,"result":true}`)
	}
}

// newChatBot builds a gating bot (ChatID set) over the fake API, without an
// eligibility resolver — each test wires the one it needs.
func newChatBot(t *testing.T, api *chatAPI) *Bot {
	t.Helper()
	srv := httptest.NewServer(api)
	t.Cleanup(srv.Close)
	b, err := New(Config{Token: "123:ABC", APIBaseURL: srv.URL, MiniAppURL: "https://example.com/", ChatID: testChatID}, zap.NewNop())
	if err != nil {
		t.Fatalf("new bot: %v", err)
	}
	b.botID = botSelfID // normally set at startup; the chat_member updates default actor 0 != this
	return b
}

// memberUpdate builds an oldType -> member transition for userID in chatID.
func memberUpdate(chatID, userID int64, oldType models.ChatMemberType) *models.ChatMemberUpdated {
	return &models.ChatMemberUpdated{
		Chat:          models.Chat{ID: chatID},
		OldChatMember: models.ChatMember{Type: oldType, Left: &models.ChatMemberLeft{User: &models.User{ID: userID}}},
		NewChatMember: models.ChatMember{Type: models.ChatMemberTypeMember, Member: &models.ChatMemberMember{User: &models.User{ID: userID}}},
	}
}

// restrictedUpdate builds an oldType -> restricted transition for userID, the new
// member's text-send permission set to canSend and membership to isMember. A muted
// member is restricted with canSend=false; an un-muted one with canSend=true.
func restrictedUpdate(chatID, userID int64, oldType models.ChatMemberType, canSend, isMember bool) *models.ChatMemberUpdated {
	return &models.ChatMemberUpdated{
		Chat:          models.Chat{ID: chatID},
		OldChatMember: models.ChatMember{Type: oldType, Left: &models.ChatMemberLeft{User: &models.User{ID: userID}}},
		NewChatMember: models.ChatMember{Type: models.ChatMemberTypeRestricted, Restricted: &models.ChatMemberRestricted{User: &models.User{ID: userID}, CanSendMessages: canSend, IsMember: isMember}},
	}
}

// leftUpdate builds a transition to left (a leave) for userID.
func leftUpdate(chatID, userID int64) *models.ChatMemberUpdated {
	return &models.ChatMemberUpdated{
		Chat:          models.Chat{ID: chatID},
		OldChatMember: models.ChatMember{Type: models.ChatMemberTypeRestricted, Restricted: &models.ChatMemberRestricted{User: &models.User{ID: userID}}},
		NewChatMember: models.ChatMember{Type: models.ChatMemberTypeLeft, Left: &models.ChatMemberLeft{User: &models.User{ID: userID}}},
	}
}

// eligibleBot builds a gating bot whose resolver returns (eligible, err).
func eligibleBot(t *testing.T, api *chatAPI, eligible bool, err error) *Bot {
	t.Helper()
	b := newChatBot(t, api)
	b.SetEligibilityResolver(func(context.Context, string) (bool, error) { return eligible, err })
	return b
}

func TestHandleChatMemberMutesIneligibleMember(t *testing.T) {
	// An unregistered/blocked member can send by the permissive default, so the bot mutes.
	api := &chatAPI{}
	b := eligibleBot(t, api, false, nil)
	b.handleChatMember(context.Background(), memberUpdate(testChatID, 777, models.ChatMemberTypeLeft))
	if len(api.restricts) != 1 || api.restricts[0].userID != "777" || api.restricts[0].canSend {
		t.Fatalf("restricts = %+v, want one mute (can_send=false) for 777", api.restricts)
	}
}

func TestHandleChatMemberLeavesEligibleMemberAlone(t *testing.T) {
	// An eligible plain member already sends (the permissive default); no action needed.
	api := &chatAPI{}
	b := eligibleBot(t, api, true, nil)
	b.handleChatMember(context.Background(), memberUpdate(testChatID, 777, models.ChatMemberTypeLeft))
	if len(api.restricts) != 0 {
		t.Fatalf("restricts = %+v, want none for an eligible member (already allowed)", api.restricts)
	}
}

func TestHandleChatMemberUnmutesEligibleRestricted(t *testing.T) {
	// An eligible member the bot had muted (restricted, can_send=false) is restored.
	api := &chatAPI{}
	b := eligibleBot(t, api, true, nil)
	b.handleChatMember(context.Background(), restrictedUpdate(testChatID, 777, models.ChatMemberTypeRestricted, false, true))
	if len(api.restricts) != 1 || !api.restricts[0].canSend {
		t.Fatalf("restricts = %+v, want one un-mute (can_send=true)", api.restricts)
	}
}

func TestHandleChatMemberLeavesEligibleAllowedRestrictedAlone(t *testing.T) {
	// An eligible restricted member who can already send needs no change — the real case
	// from the contour (restricted, can_send=true, eligible).
	api := &chatAPI{}
	b := eligibleBot(t, api, true, nil)
	b.handleChatMember(context.Background(), restrictedUpdate(testChatID, 777, models.ChatMemberTypeRestricted, true, true))
	if len(api.restricts) != 0 {
		t.Fatalf("restricts = %+v, want none for an eligible already-allowed member", api.restricts)
	}
}

func TestHandleChatMemberMutesIneligibleRestricted(t *testing.T) {
	// An ineligible member who can still send is muted.
	api := &chatAPI{}
	b := eligibleBot(t, api, false, nil)
	b.handleChatMember(context.Background(), restrictedUpdate(testChatID, 777, models.ChatMemberTypeRestricted, true, true))
	if len(api.restricts) != 1 || api.restricts[0].canSend {
		t.Fatalf("restricts = %+v, want one mute (can_send=false)", api.restricts)
	}
}

func TestHandleChatMemberSkipsNonMember(t *testing.T) {
	// A restricted record for a user no longer in the chat (is_member=false) is not acted on.
	api := &chatAPI{}
	b := eligibleBot(t, api, false, nil)
	b.handleChatMember(context.Background(), restrictedUpdate(testChatID, 777, models.ChatMemberTypeRestricted, true, false))
	if len(api.restricts) != 0 {
		t.Fatalf("restricts = %+v, want none for a non-member", api.restricts)
	}
}

func TestHandleChatMemberSkipsBotsOwnAction(t *testing.T) {
	// The bot's own restrict re-fires a chat_member update performed by the bot; skip it
	// so an action never loops (the resolver here would otherwise mute).
	api := &chatAPI{}
	b := eligibleBot(t, api, false, nil)
	upd := memberUpdate(testChatID, 777, models.ChatMemberTypeLeft)
	upd.From = models.User{ID: botSelfID}
	b.handleChatMember(context.Background(), upd)
	if len(api.restricts) != 0 {
		t.Fatalf("restricts = %+v, want none for the bot's own action (no loop)", api.restricts)
	}
}

func TestHandleChatMemberNoChangeOnResolveError(t *testing.T) {
	api := &chatAPI{}
	b := eligibleBot(t, api, false, context.DeadlineExceeded)
	b.handleChatMember(context.Background(), memberUpdate(testChatID, 777, models.ChatMemberTypeLeft))
	if len(api.restricts) != 0 {
		t.Fatalf("a resolve error still changed access: %+v (want no change)", api.restricts)
	}
}

func TestHandleChatMemberIgnoresOtherChatAndLeaves(t *testing.T) {
	api := &chatAPI{}
	b := eligibleBot(t, api, false, nil) // ineligible — would mute if it acted
	ctx := context.Background()
	b.handleChatMember(ctx, memberUpdate(999, 777, models.ChatMemberTypeLeft)) // foreign chat
	b.handleChatMember(ctx, leftUpdate(testChatID, 777))                       // a leave
	if len(api.restricts) != 0 {
		t.Fatalf("restricts = %+v, want none for a foreign chat / a leave", api.restricts)
	}
}

func TestApplyChatGatePresentMember(t *testing.T) {
	for _, tc := range []struct {
		name  string
		allow bool
	}{{"grant", true}, {"mute", false}} {
		t.Run(tc.name, func(t *testing.T) {
			api := &chatAPI{memberStatus: "member"}
			b := newChatBot(t, api)
			applied, err := b.ApplyChatGate(context.Background(), 777, tc.allow)
			if err != nil {
				t.Fatalf("apply: %v", err)
			}
			if !applied {
				t.Fatal("applied = false, want true for a present member")
			}
			if len(api.restricts) != 1 || api.restricts[0].canSend != tc.allow {
				t.Fatalf("restricts = %+v, want one with can_send=%v", api.restricts, tc.allow)
			}
		})
	}
}

func TestApplyChatGateSkipsAbsentAndAdmin(t *testing.T) {
	for _, status := range []string{"left", "kicked", "administrator", "creator"} {
		t.Run(status, func(t *testing.T) {
			api := &chatAPI{memberStatus: status}
			b := newChatBot(t, api)
			applied, err := b.ApplyChatGate(context.Background(), 777, false)
			if err != nil {
				t.Fatalf("apply: %v", err)
			}
			if applied {
				t.Errorf("applied = true for status %q, want a no-op", status)
			}
			if len(api.restricts) != 0 {
				t.Errorf("restricts = %+v for status %q, want none", api.restricts, status)
			}
		})
	}
}

// autoForwardUpdate builds a message update as it lands in the discussion chat: a post in
// chat chatID with id msgID and is_automatic_forward set to auto.
func autoForwardUpdate(chatID int64, msgID int, auto bool) *models.Update {
	return &models.Update{Message: &models.Message{
		ID:                 msgID,
		Chat:               models.Chat{ID: chatID, Type: models.ChatTypeSupergroup},
		IsAutomaticForward: auto,
	}}
}

func TestAutoForwardUnpinned(t *testing.T) {
	// A channel post auto-forwarded into the moderated chat is unpinned by its exact id.
	api := &chatAPI{}
	b := newChatBot(t, api)
	b.handleUpdate(context.Background(), b.api, autoForwardUpdate(testChatID, 42, true))
	if len(api.unpins) != 1 || api.unpins[0].chatID != "555" || api.unpins[0].messageID != "42" {
		t.Fatalf("unpins = %+v, want one {555, 42}", api.unpins)
	}
}

func TestNonAutoForwardNotUnpinned(t *testing.T) {
	// A normal message in the moderated chat (e.g. another admin's pinned message) is never
	// unpinned — the is_automatic_forward filter is what guards every other pin.
	api := &chatAPI{}
	b := newChatBot(t, api)
	b.handleUpdate(context.Background(), b.api, autoForwardUpdate(testChatID, 42, false))
	if len(api.unpins) != 0 {
		t.Fatalf("unpins = %+v, want none for a non-auto-forward message", api.unpins)
	}
}

func TestAutoForwardOtherChatIgnored(t *testing.T) {
	// An auto-forward in some other chat (not the configured moderated one) is ignored.
	api := &chatAPI{}
	b := newChatBot(t, api)
	b.handleUpdate(context.Background(), b.api, autoForwardUpdate(999, 42, true))
	if len(api.unpins) != 0 {
		t.Fatalf("unpins = %+v, want none for a foreign chat", api.unpins)
	}
}
