package cmdpacks_test

import (
	"fmt"
	"testing"

	tea "github.com/charmbracelet/bubbletea"
	bus "github.com/lmika/events"
	"github.com/stretchr/testify/assert"
	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl/cmdpacks"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/dynamo"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/settingstore"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/workspacestore"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/inputhistory"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/jobs"
	keybindings_service "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/keybindings"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/keybindings"
	"lmika.dev/cmd/dynamo-browse/test/testdynamo"
	"lmika.dev/cmd/dynamo-browse/test/testworkspace"
)

func TestStdCmds_Mark(t *testing.T) {
	tests := []struct {
		descr     string
		cmd       string
		wantMarks []bool
	}{
		{descr: "mark default", cmd: "mark", wantMarks: []bool{true, true, true}},
		{descr: "mark all", cmd: "mark all", wantMarks: []bool{true, true, true}},
		{descr: "mark none", cmd: "mark none", wantMarks: []bool{false, false, false}},
		{descr: "mark where", cmd: `mark -where 'sk="222"'`, wantMarks: []bool{false, true, false}},
		{descr: "mark toggle", cmd: "mark ; mark toggle", wantMarks: []bool{false, false, false}},
	}

	for _, tt := range tests {
		t.Run(tt.descr, func(t *testing.T) {
			svc := newService(t)

			_, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd)
			assert.NoError(t, err)

			for i, want := range tt.wantMarks {
				assert.Equal(t, want, svc.State.ResultSet().Marked(i))
			}
		})
	}
}

type testDataGenerator func() []testdynamo.TestData

type testPasteBoard struct {
	content string
}

func (t *testPasteBoard) ReadText() (string, bool) {
	return t.content, true
}

func (t *testPasteBoard) WriteText(bts []byte) error {
	t.content = string(bts)
	return nil
}

type services struct {
	CommandController *commandctrl.CommandController
	SelItemIndex      int

	State *controllers.State

	settingStore *settingstore.SettingStore
	table        string
	pasteboard   *testPasteBoard

	testDataGenerator testDataGenerator
	testData          []testdynamo.TestData
}

type serviceOpt func(*services)

func withDataGenerator(tg testDataGenerator) serviceOpt {
	return func(s *services) {
		s.testDataGenerator = tg
	}
}

func withTable(table string) serviceOpt {
	return func(s *services) {
		s.table = table
	}
}

func withDefaultLimit(limit int) serviceOpt {
	return func(s *services) {
		s.settingStore.SetDefaultLimit(limit)
	}
}

func newService(t *testing.T, opts ...serviceOpt) *services {
	ws := testworkspace.New(t)

	resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
	settingStore := settingstore.New(ws)
	inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)

	workspaceService := viewsnapshot.NewService(resultSetSnapshotStore)
	itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
	inputHistoryService := inputhistory.New(inputHistoryStore)

	testPB := &testPasteBoard{}
	s := &services{
		table:             "service-test-data",
		settingStore:      settingStore,
		testDataGenerator: normalTestData,
		pasteboard:        testPB,
	}

	for _, opt := range opts {
		opt(s)
	}

	s.testData = s.testDataGenerator()
	client := testdynamo.SetupTestTable(t, s.testData)

	provider := dynamo.NewProvider(client)
	service := tables.NewService(provider, settingStore)
	eventBus := bus.New()

	state := controllers.NewState()
	jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)

	readController := controllers.NewTableReadController(
		state,
		service,
		workspaceService,
		itemRendererService,
		jobsController,
		inputHistoryService,
		eventBus,
		testPB,
		s.table,
	)
	writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
	settingsController := controllers.NewSettingsController(settingStore, eventBus)
	columnsController := controllers.NewColumnsController(readController, eventBus)
	exportController := controllers.NewExportController(state, service, jobsController, columnsController, testPB)

	keyBindingService := keybindings_service.NewService(keybindings.Default())
	keyBindingController := controllers.NewKeyBindingController(keyBindingService, nil)

	commandController, _ := commandctrl.NewCommandController(inputHistoryService,
		cmdpacks.NewStandardCommands(
			service,
			state,
			readController,
			writeController,
			exportController,
			keyBindingController,
			testPB,
			settingsController,
			itemRendererService,
		),
	)

	s.State = state
	s.CommandController = commandController

	commandController.SetUIStateProvider(s)
	readController.Init()

	return s
}

func (s *services) SelectedItemIndex() int {
	return s.SelItemIndex
}

func (s *services) SetSelectedItemIndex(newIdx int) tea.Msg {
	s.SelItemIndex = newIdx
	return nil
}

func normalTestData() []testdynamo.TestData {
	return []testdynamo.TestData{
		{
			TableName: "service-test-data",
			Data: []map[string]interface{}{
				{
					"pk":    "abc",
					"sk":    "111",
					"alpha": "This is some value",
				},
				{
					"pk":    "abc",
					"sk":    "222",
					"alpha": "This is another some value",
					"beta":  1231,
				},
				{
					"pk":    "bbb",
					"sk":    "131",
					"beta":  2468,
					"gamma": "foobar",
				},
			},
		},
	}
}

func largeTestData() []testdynamo.TestData {
	return []testdynamo.TestData{
		{
			TableName: "large-table",
			Data: genRow(50, func(i int) map[string]interface{} {
				return map[string]interface{}{
					"pk":    fmt.Sprint(i),
					"sk":    fmt.Sprint(i),
					"alpha": fmt.Sprintf("row %v", i),
				}
			}),
		},
	}
}

func genRow(count int, mapFn func(int) map[string]interface{}) []map[string]interface{} {
	result := make([]map[string]interface{}, count)
	for i := 0; i < count; i++ {
		result[i] = mapFn(i)
	}
	return result
}
