package scripting

import (
	"testing"

	lua "github.com/yuin/gopher-lua"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/idursun/jjui/internal/ui/common"
	uicontext "github.com/idursun/jjui/internal/ui/context"
	"github.com/idursun/jjui/internal/ui/intents"
)

func strPtr(v string) *string {
	return &v
}

func assertLuaStringOrNil(t *testing.T, val lua.LValue, expected *string) {
	t.Helper()
	if expected == nil {
		assert.Equal(t, lua.LNil, val)
		return
	}
	assert.Equal(t, *expected, val.String())
}

// runScriptAndGetGlobal runs a Lua script and returns the value of a global variable
// before the Lua state is closed.
func runScriptAndGetGlobal(t *testing.T, ctx *uicontext.MainContext, script, varName string) lua.LValue {
	results := runScriptAndGetGlobals(t, ctx, script, varName)
	if len(results) > 0 {
		return results[0]
	}
	return lua.LNil
}

func runScriptAndGetGlobals(t *testing.T, ctx *uicontext.MainContext, script string, varNames ...string) []lua.LValue {
	L := lua.NewState()
	defer L.Close()

	registerAPI(L, ctx)

	fn, err := L.LoadString(script)
	assert.NoError(t, err)

	err = L.CallByParam(lua.P{
		Fn:      fn,
		NRet:    0,
		Protect: true,
	})
	assert.NoError(t, err)

	var results []lua.LValue
	for _, name := range varNames {
		results = append(results, L.GetGlobal(name))
	}
	return results
}

func TestContext_ChangeId(t *testing.T) {
	tests := []struct {
		name string
		ctx  *uicontext.MainContext
		want *string
	}{
		{
			name: "selected revision",
			ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedRevision{
				ChangeId: "abc123",
				CommitId: "def456",
			}},
			want: strPtr("abc123"),
		},
		{
			name: "selected file",
			ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedFile{
				ChangeId: "file123",
				CommitId: "commit456",
				File:     "test.go",
			}},
			want: strPtr("file123"),
		},
		{
			name: "no selection",
			ctx:  &uicontext.MainContext{},
			want: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			val := runScriptAndGetGlobal(t, tt.ctx, `result = context.change_id()`, "result")
			assertLuaStringOrNil(t, val, tt.want)
		})
	}
}

func TestContext_CommitId(t *testing.T) {
	tests := []struct {
		name string
		ctx  *uicontext.MainContext
		want *string
	}{
		{
			name: "selected revision",
			ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedRevision{
				ChangeId: "abc123",
				CommitId: "def456",
			}},
			want: strPtr("def456"),
		},
		{
			name: "selected file",
			ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedFile{
				ChangeId: "file123",
				CommitId: "commit456",
				File:     "test.go",
			}},
			want: strPtr("commit456"),
		},
		{
			name: "selected commit",
			ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedCommit{
				CommitId: "onlycommit789",
			}},
			want: strPtr("onlycommit789"),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			val := runScriptAndGetGlobal(t, tt.ctx, `result = context.commit_id()`, "result")
			assertLuaStringOrNil(t, val, tt.want)
		})
	}
}

func TestContext_File(t *testing.T) {
	tests := []struct {
		name string
		ctx  *uicontext.MainContext
		want *string
	}{
		{
			name: "selected file",
			ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedFile{
				ChangeId: "file123",
				CommitId: "commit456",
				File:     "path/to/file.go",
			}},
			want: strPtr("path/to/file.go"),
		},
		{
			name: "selected revision",
			ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedRevision{
				ChangeId: "abc123",
				CommitId: "def456",
			}},
			want: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			val := runScriptAndGetGlobal(t, tt.ctx, `result = context.file()`, "result")
			assertLuaStringOrNil(t, val, tt.want)
		})
	}
}

func TestContext_OperationId(t *testing.T) {
	tests := []struct {
		name string
		ctx  *uicontext.MainContext
		want *string
	}{
		{
			name: "selected operation",
			ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedOperation{
				OperationId: "op123456",
			}},
			want: strPtr("op123456"),
		},
		{
			name: "selected revision",
			ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedRevision{
				ChangeId: "abc123",
				CommitId: "def456",
			}},
			want: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			val := runScriptAndGetGlobal(t, tt.ctx, `result = context.operation_id()`, "result")
			assertLuaStringOrNil(t, val, tt.want)
		})
	}
}

func TestContext_CheckedFiles(t *testing.T) {
	ctx := &uicontext.MainContext{
		CheckedItems: []uicontext.SelectedItem{
			uicontext.SelectedFile{ChangeId: "c1", CommitId: "co1", File: "file1.go"},
			uicontext.SelectedFile{ChangeId: "c2", CommitId: "co2", File: "file2.go"},
			uicontext.SelectedRevision{ChangeId: "rev1", CommitId: "com1"}, // should be ignored
			uicontext.SelectedFile{ChangeId: "c3", CommitId: "co3", File: "file3.go"},
		},
	}

	vals := runScriptAndGetGlobals(t, ctx, `
		files = context.checked_files()
		count = #files
		first = files[1]
		second = files[2]
		third = files[3]
	`, "count", "first", "second", "third")

	assert.Equal(t, lua.LNumber(3), vals[0])
	assert.Equal(t, "file1.go", vals[1].String())
	assert.Equal(t, "file2.go", vals[2].String())
	assert.Equal(t, "file3.go", vals[3].String())
}

func TestContext_CheckedFiles_Empty(t *testing.T) {
	ctx := &uicontext.MainContext{
		CheckedItems: []uicontext.SelectedItem{},
	}

	val := runScriptAndGetGlobal(t, ctx, `
		files = context.checked_files()
		result = #files
	`, "result")

	assert.Equal(t, lua.LNumber(0), val)
}

func TestContext_CheckedChangeIds(t *testing.T) {
	ctx := &uicontext.MainContext{
		CheckedItems: []uicontext.SelectedItem{
			uicontext.SelectedRevision{ChangeId: "change1", CommitId: "com1"},
			uicontext.SelectedFile{ChangeId: "change2", CommitId: "com2", File: "f.go"},
			uicontext.SelectedOperation{OperationId: "op1"}, // should be ignored
			uicontext.SelectedRevision{ChangeId: "change3", CommitId: "com3"},
		},
	}

	vals := runScriptAndGetGlobals(t, ctx, `
		ids = context.checked_change_ids()
		count = #ids
		first = ids[1]
		second = ids[2]
		third = ids[3]
	`, "count", "first", "second", "third")

	assert.Equal(t, lua.LNumber(3), vals[0])
	assert.Equal(t, "change1", vals[1].String())
	assert.Equal(t, "change2", vals[2].String())
	assert.Equal(t, "change3", vals[3].String())
}

func TestContext_CheckedCommitIds(t *testing.T) {
	ctx := &uicontext.MainContext{
		CheckedItems: []uicontext.SelectedItem{
			uicontext.SelectedRevision{ChangeId: "c1", CommitId: "commit1"},
			uicontext.SelectedFile{ChangeId: "c2", CommitId: "commit2", File: "f.go"},
			uicontext.SelectedCommit{CommitId: "commit3"},
			uicontext.SelectedOperation{OperationId: "op1"}, // should be ignored
		},
	}

	vals := runScriptAndGetGlobals(t, ctx, `
		ids = context.checked_commit_ids()
		count = #ids
		first = ids[1]
		second = ids[2]
		third = ids[3]
	`, "count", "first", "second", "third")

	assert.Equal(t, lua.LNumber(3), vals[0])
	assert.Equal(t, "commit1", vals[1].String())
	assert.Equal(t, "commit2", vals[2].String())
	assert.Equal(t, "commit3", vals[3].String())
}

func TestContext_AccessViaJjuiNamespace(t *testing.T) {
	ctx := &uicontext.MainContext{
		SelectedItem: uicontext.SelectedRevision{
			ChangeId: "ns_change",
			CommitId: "ns_commit",
		},
	}

	vals := runScriptAndGetGlobals(t, ctx, `
		change = jjui.context.change_id()
		commit = jjui.context.commit_id()
	`, "change", "commit")

	assert.Equal(t, "ns_change", vals[0].String())
	assert.Equal(t, "ns_commit", vals[1].String())
}

func TestGeneratedActions_AccessViaJjuiNamespace(t *testing.T) {
	ctx := &uicontext.MainContext{}

	vals := runScriptAndGetGlobals(t, ctx, `
		result_details = type(jjui.revisions.details.cancel)
		result_nested = type(jjui.revisions.details.confirmation.apply)
		result_builtin = type(jjui.builtin.revisions.details.cancel)
		result_legacy = type(jjui.action)
	`, "result_details", "result_nested", "result_builtin", "result_legacy")

	assert.Equal(t, "function", vals[0].String())
	assert.Equal(t, "function", vals[1].String())
	assert.Equal(t, "function", vals[2].String())
	assert.Equal(t, "nil", vals[3].String())
}

func TestWaitHelpers_AccessViaTopLevelAndJjuiNamespace(t *testing.T) {
	ctx := &uicontext.MainContext{}

	vals := runScriptAndGetGlobals(t, ctx, `
		top_close = type(wait_close)
		top_refresh = type(wait_refresh)
		ns_close = type(jjui.wait_close)
		ns_refresh = type(jjui.wait_refresh)
	`, "top_close", "top_refresh", "ns_close", "ns_refresh")

	assert.Equal(t, "function", vals[0].String())
	assert.Equal(t, "function", vals[1].String())
	assert.Equal(t, "function", vals[2].String())
	assert.Equal(t, "function", vals[3].String())
}

func TestSetThemeDispatchesChangeThemeIntent(t *testing.T) {
	ctx := setupVM(t)

	runner, cmd, err := RunScript(ctx, `set_theme("runtime_dark")`)
	require.NoError(t, err)
	require.NotNil(t, runner)
	require.NotNil(t, cmd)

	msg := cmd()
	intent, ok := msg.(intents.ChangeTheme)
	require.True(t, ok)
	assert.Equal(t, "runtime_dark", intent.Name)
}

func TestSetThemeAccessibleViaJjuiNamespace(t *testing.T) {
	ctx := setupVM(t)

	runner, cmd, err := RunScript(ctx, `jjui.set_theme("runtime_light")`)
	require.NoError(t, err)
	require.NotNil(t, runner)
	require.NotNil(t, cmd)

	msg := cmd()
	intent, ok := msg.(intents.ChangeTheme)
	require.True(t, ok)
	assert.Equal(t, "runtime_light", intent.Name)
}

func runWaitingScript(t *testing.T, script string) (*uicontext.MainContext, *Runner) {
	t.Helper()

	ctx := setupVM(t)
	runner, cmd, err := RunScript(ctx, script)
	require.NoError(t, err)
	require.NotNil(t, runner)
	if cmd != nil {
		_ = cmd()
	}
	require.False(t, runner.Done())

	return ctx, runner
}

func TestRunner_WaitClose(t *testing.T) {
	t.Run("ignores non-close messages and resumes on close", func(t *testing.T) {
		ctx, runner := runWaitingScript(t, `
			done = false
			applied = wait_close()
			done = true
		`)

		cmd := runner.HandleMsg(common.UpdateRevisionsSuccessMsg{})
		assert.Nil(t, cmd)
		assert.False(t, runner.Done())
		assert.Equal(t, "false", ctx.ScriptVM.GetGlobal("done").String())

		cmd = runner.HandleMsg(common.CloseViewMsg{Applied: true})
		if cmd != nil {
			_ = cmd()
		}
		assert.True(t, runner.Done())
		assert.Equal(t, "true", ctx.ScriptVM.GetGlobal("done").String())
		assert.Equal(t, "true", ctx.ScriptVM.GetGlobal("applied").String())
	})

	tests := []struct {
		name    string
		applied bool
	}{
		{name: "applied", applied: true},
		{name: "cancelled", applied: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx, runner := runWaitingScript(t, `applied = wait_close()`)

			cmd := runner.HandleMsg(common.CloseViewMsg{Applied: tt.applied})
			if cmd != nil {
				_ = cmd()
			}

			assert.True(t, runner.Done())
			assert.Equal(t, tt.applied, ctx.ScriptVM.GetGlobal("applied") == lua.LTrue)
		})
	}
}

func TestRunner_WaitRefreshResumesOnUpdateRevisions(t *testing.T) {
	tests := []struct {
		name string
		msg  any
	}{
		{name: "success", msg: common.UpdateRevisionsSuccessMsg{}},
		{name: "failed", msg: common.UpdateRevisionsFailedMsg{Output: "err"}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx, runner := runWaitingScript(t, `
				done = false
				wait_refresh()
				done = true
			`)

			cmd := runner.HandleMsg(tt.msg)
			if cmd != nil {
				_ = cmd()
			}
			assert.True(t, runner.Done())
			assert.Equal(t, "true", ctx.ScriptVM.GetGlobal("done").String())
		})
	}
}
