package mcp_service

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	"evening_detective_server/internal/modules/storytelling"
	"evening_detective_server/internal/services/game_service"

	"github.com/mark3labs/mcp-go/client"
	"github.com/mark3labs/mcp-go/mcp"
)

// fakeGamePlayer — мок GamePlayer с предзаданными историей, игрой и
// ошибками (реализует интерфейс GamePlayer, БД не нужна).
type fakeGamePlayer struct {
	story *storytelling.Story
	game  *game_service.Game
	err   error
}

func (f *fakeGamePlayer) GetTeamActions(_ context.Context, teamID int, password string) (*storytelling.Story, *game_service.Game, error) {
	if f.err != nil {
		return nil, nil, f.err
	}
	return f.story, f.game, nil
}

func (f *fakeGamePlayer) AddTeamAction(_ context.Context, teamID int, password, code string) error {
	if f.err != nil {
		return f.err
	}
	return nil
}

func defaultStory() *storytelling.Story {
	return &storytelling.Story{
		Introduction: &storytelling.Introduction{
			Text:  "Добро пожаловать в особняк.",
			Audio: "http://storage.test/api/files/intro.mp3",
		},
		Places: []*storytelling.Place{
			{
				Code: "entrance",
				Name: "Вход",
				Text: "Вы у входа в особняк.",
				Doors: []*storytelling.Door{
					{Code: "hall", Name: "Зал"},
				},
			},
		},
	}
}

func defaultGame() *game_service.Game {
	return &game_service.Game{
		ID:     1,
		Name:   "Игра 1",
		Status: "started",
	}
}

// mcpFixture поднимает in-process MCP-клиент над сервисом с моком
// GamePlayer. Каждый кейс создаёт СВЕЖИЙ сервис.
type mcpFixture struct {
	client *client.Client
}

func newMCPFixture(t *testing.T, player GamePlayer) *mcpFixture {
	t.Helper()

	svc := NewMCPService(player)

	mcpClient, err := client.NewInProcessClient(svc.Server())
	if err != nil {
		t.Fatalf("NewInProcessClient() error = %v", err)
	}
	t.Cleanup(func() { _ = mcpClient.Close() })

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	t.Cleanup(cancel)

	if err := mcpClient.Start(ctx); err != nil {
		t.Fatalf("client.Start() error = %v", err)
	}

	initRequest := mcp.InitializeRequest{}
	initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
	initRequest.Params.ClientInfo = mcp.Implementation{Name: "mcp_service_test", Version: "1.0.0"}
	if _, err := mcpClient.Initialize(ctx, initRequest); err != nil {
		t.Fatalf("client.Initialize() error = %v", err)
	}

	return &mcpFixture{client: mcpClient}
}

// call вызывает инструмент с аргументами и возвращает текст результата.
func (f *mcpFixture) call(t *testing.T, name string, args map[string]any) string {
	t.Helper()
	request := mcp.CallToolRequest{}
	request.Params.Name = name
	request.Params.Arguments = args

	result, err := f.client.CallTool(context.Background(), request)
	if err != nil {
		t.Fatalf("CallTool(%s) error = %v", name, err)
	}
	var text strings.Builder
	for _, content := range result.Content {
		switch c := content.(type) {
		case mcp.TextContent:
			text.WriteString(c.Text)
		default:
			t.Fatalf("CallTool(%s) unexpected content type %T", name, content)
		}
	}
	return text.String()
}

// toolNames возвращает имена инструментов сервера.
func (f *mcpFixture) toolNames(t *testing.T) []string {
	t.Helper()
	toolsResult, err := f.client.ListTools(context.Background(), mcp.ListToolsRequest{})
	if err != nil {
		t.Fatalf("ListTools() error = %v", err)
	}
	names := make([]string, 0, len(toolsResult.Tools))
	for _, tool := range toolsResult.Tools {
		names = append(names, tool.Name)
	}
	return names
}

func assertExactTools(t *testing.T, names []string) {
	t.Helper()
	if len(names) != 3 {
		t.Fatalf("tools = %v; want ровно 3 инструмента", names)
	}
	for _, want := range []string{"connect", "get_team_story", "make_move"} {
		found := false
		for _, name := range names {
			if name == want {
				found = true
				break
			}
		}
		if !found {
			t.Errorf("tools = %v; не содержит %q", names, want)
		}
	}
}

func TestServerListsTools(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{story: defaultStory(), game: defaultGame()})
	assertExactTools(t, f.toolNames(t))
}

func TestConnectRelativeURL(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{story: defaultStory(), game: defaultGame()})

	got := f.call(t, "connect", map[string]any{
		"url": "/team-story/10?password=team-pass-1",
	})
	if !strings.Contains(got, "Подключено к команде 10") {
		t.Fatalf("connect: %q; want team id in message", got)
	}
	if !strings.Contains(got, "team-pass-1") {
		t.Fatalf("connect: %q; want password echoed", got)
	}
	if !strings.Contains(got, "entrance") {
		t.Fatalf("connect: %q; want story JSON", got)
	}
	if !strings.Contains(got, `"game":{"id":1,"name":"Игра 1","status":"started"}`) {
		t.Fatalf("connect: %q; want game DTO", got)
	}
}

// TestConnectProtocolRelativeURL: протокол-относительная ссылка принимается
// (запросы по ней не выполняются — из ссылки берутся только id и пароль).
func TestConnectProtocolRelativeURL(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{story: defaultStory(), game: defaultGame()})

	got := f.call(t, "connect", map[string]any{
		"url": "//host/team-story/10?password=team-pass-1",
	})
	if !strings.Contains(got, "Подключено к команде 10") {
		t.Fatalf("connect: %q; want success", got)
	}
}

func TestConnectFullURL(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{story: defaultStory(), game: defaultGame()})

	got := f.call(t, "connect", map[string]any{
		"url": "http://any.example:9999/team-story/10?password=team-pass-1",
	})
	if !strings.Contains(got, "Подключено к команде 10") {
		t.Fatalf("connect: %q; want success (хост игнорируется)", got)
	}
}

func TestConnectMissingURL(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{})

	got := f.call(t, "connect", map[string]any{})
	if !strings.Contains(got, "требуется url") {
		t.Fatalf("connect без url: %q; want args hint", got)
	}
}

func TestConnectInvalidURLs(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{})

	cases := []struct {
		name string
		url  string
		want string
	}{
		{"пустой password", "/team-story/10", "password"},
		{"не тот путь", "/game/10?password=x", "путь должен быть"},
		{"mailto-схема", "mailto:team@example.com", "путь должен быть"},
		{"id не число", "/team-story/abc?password=x", "id команды"},
		{"отрицательный id", "/team-story/-5?password=x", "id команды"},
		{"ошибка url.Parse в пути", "/team-story/%zz?password=x", "невалидная ссылка"},
		{"нет id в пути", "/team-story/?password=x", "отсутствует id команды"},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := f.call(t, "connect", map[string]any{"url": tc.url})
			if !strings.Contains(got, tc.want) {
				t.Fatalf("connect(%q): %q; want подстроку %q", tc.url, got, tc.want)
			}
		})
	}
}

func TestConnectBusinessError(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{err: errors.New("team not found")})

	got := f.call(t, "connect", map[string]any{
		"url": "/team-story/10?password=wrong",
	})
	if !strings.Contains(got, "team not found") {
		t.Fatalf("connect с ошибкой сервиса: %q; want error text", got)
	}
}

func TestGetTeamStory(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{story: defaultStory(), game: defaultGame()})

	got := f.call(t, "get_team_story", map[string]any{
		"team_id":  float64(10),
		"password": "team-pass-1",
	})
	if !strings.Contains(got, "entrance") {
		t.Fatalf("get_team_story: %q; want story JSON", got)
	}
	if !strings.Contains(got, `"introduction"`) || !strings.Contains(got, "Добро пожаловать в особняк.") {
		t.Fatalf("get_team_story: %q; want introduction in story JSON", got)
	}
}

func TestGetTeamStoryBusinessError(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{err: errors.New("team not found")})

	got := f.call(t, "get_team_story", map[string]any{
		"team_id":  float64(10),
		"password": "wrong",
	})
	if !strings.Contains(got, "team not found") {
		t.Fatalf("get_team_story с ошибкой сервиса: %q; want error text", got)
	}
}

func TestGetTeamStoryInvalidTeamID(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{})

	got := f.call(t, "get_team_story", map[string]any{
		"team_id":  "not-a-number",
		"password": "x",
	})
	if !strings.Contains(got, "team_id") {
		t.Fatalf("get_team_story с нечисловым team_id: %q; want args hint", got)
	}
}

func TestMakeMove(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{story: defaultStory(), game: defaultGame()})

	got := f.call(t, "make_move", map[string]any{
		"team_id":  "10", // строка тоже принимается
		"password": "team-pass-1",
		"code":     "hall",
	})
	if !strings.Contains(got, "entrance") {
		t.Fatalf("make_move: %q; want updated story JSON", got)
	}
}

func TestMakeMoveActionError(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{err: errors.New("code is forbidden")})

	got := f.call(t, "make_move", map[string]any{
		"team_id":  float64(10),
		"password": "team-pass-1",
		"code":     "forbidden",
	})
	if !strings.Contains(got, "code is forbidden") {
		t.Fatalf("make_move: %q; want error text", got)
	}
}

func TestMakeMoveMissingCode(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{})

	got := f.call(t, "make_move", map[string]any{
		"team_id":  float64(10),
		"password": "team-pass-1",
		"code":     "",
	})
	if !strings.Contains(got, "password и code") {
		t.Fatalf("make_move без code: %q; want args hint", got)
	}
}

func TestMakeMoveWithoutTeamID(t *testing.T) {
	f := newMCPFixture(t, &fakeGamePlayer{})

	got := f.call(t, "make_move", map[string]any{
		"password": "x",
		"code":     "hall",
	})
	if !strings.Contains(got, "team_id") {
		t.Fatalf("make_move без team_id: %q; want args hint", got)
	}
}

func TestParseConnectURL(t *testing.T) {
	okCases := []struct {
		name     string
		url      string
		teamID   int64
		password string
	}{
		{"относительная", "/team-story/10?password=p1", 10, "p1"},
		{"полный URL", "http://host:8090/team-story/7?password=p2", 7, "p2"},
		{"протокол-относительная", "//host/team-story/3?password=p3", 3, "p3"},
		{"лишний query", "/team-story/1?password=p4&foo=bar", 1, "p4"},
	}
	for _, tc := range okCases {
		t.Run(tc.name, func(t *testing.T) {
			args, err := parseConnectURL(tc.url)
			if err != nil {
				t.Fatalf("parseConnectURL(%q) error = %v", tc.url, err)
			}
			if args.teamID != tc.teamID || args.password != tc.password {
				t.Fatalf("parseConnectURL(%q) = %+v; want teamID=%d password=%q", tc.url, args, tc.teamID, tc.password)
			}
		})
	}

	errCases := []struct {
		name string
		url  string
	}{
		{"пустая", ""},
		{"пробелы", "   "},
		{"нет password", "/team-story/10"},
		{"не тот путь", "/game/10?password=x"},
		{"id не число", "/team-story/abc?password=x"},
		{"отрицательный id", "/team-story/-5?password=x"},
		{"ошибка парсинга", "/team-story/%zz?password=x"},
	}
	for _, tc := range errCases {
		t.Run(tc.name, func(t *testing.T) {
			if _, err := parseConnectURL(tc.url); err == nil {
				t.Fatalf("parseConnectURL(%q) error = nil; want error", tc.url)
			}
		})
	}
}

// TestNewMCPService — конструктор не требует БД и возвращает сервис.
func TestNewMCPService(t *testing.T) {
	svc := NewMCPService(&fakeGamePlayer{})
	if svc == nil {
		t.Fatal("NewMCPService() = nil")
	}
	if svc.game == nil {
		t.Fatal("NewMCPService(): game == nil")
	}
}

// TestDefaultCallTimeout — защита от случайного изменения таймаута.
func TestDefaultCallTimeout(t *testing.T) {
	if defaultCallTimeout != 10*time.Second {
		t.Fatalf("defaultCallTimeout = %v; want 10s", defaultCallTimeout)
	}
}
