package api

import (
	"context"
	"testing"

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

	apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
	config "github.com/OliveTin/OliveTin/internal/config"
	"github.com/OliveTin/OliveTin/internal/entities"
	"github.com/OliveTin/OliveTin/internal/executor"
)

func buildEntityArgumentGuessConfig() *config.Config {
	cfg := config.DefaultConfig()
	cfg.DefaultPermissions.View = false
	cfg.DefaultPermissions.Exec = false
	cfg.AccessControlLists = []*config.AccessControlList{
		{
			Name:           "ops",
			MatchUsernames: []string{"admin"},
			Permissions:    config.PermissionsList{View: true, Exec: true},
		},
		{
			Name:             "everyone",
			MatchUsernames:   []string{"guest", "admin"},
			Permissions:      config.PermissionsList{View: true, Exec: true},
			AddToEveryAction: true,
		},
	}
	cfg.Entities = []*config.EntityFile{
		{Name: "servers", File: "servers.yaml", Acls: []string{"ops"}},
	}
	cfg.Actions = []*config.Action{
		{
			ID:    "reboot-server",
			Title: "Reboot server",
			Shell: "echo reboot '{{ target }}'",
			Arguments: []config.ActionArgument{
				{
					Name:   "target",
					Title:  "Server",
					Entity: "servers",
					Choices: []config.ActionArgumentChoice{
						{Title: "{{ servers.name }}", Value: "{{ servers.name }}"},
					},
				},
			},
		},
	}
	cfg.Sanitize()
	return cfg
}

func seedEntityArgumentGuessEntities(t *testing.T) {
	t.Helper()
	entities.ClearEntitiesOfType("servers")
	t.Cleanup(func() {
		entities.ClearEntitiesOfType("servers")
	})
	entities.AddEntity("servers", "0", map[string]any{"name": "web01"})
	entities.AddEntity("servers", "1", map[string]any{"name": "db01"})
}

func TestStartActionRejectsGuessedRestrictedEntityArgument(t *testing.T) {
	seedEntityArgumentGuessEntities(t)
	cfg := buildEntityArgumentGuessConfig()
	ex := executor.DefaultExecutor(cfg)
	ex.RebuildActionMap()
	ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
	defer ts.Close()

	_, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
		BindingId: "reboot-server",
		Arguments: []*apiv1.StartActionArgument{
			{Name: "target", Value: "web01"},
		},
	}))
	require.Error(t, err)
	assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
}

func TestStartActionRejectsUnknownEntityArgumentValue(t *testing.T) {
	seedEntityArgumentGuessEntities(t)
	cfg := buildEntityArgumentGuessConfig()
	// Make servers unrestricted so guests can view the type but not invent values.
	cfg.Entities[0].Acls = nil
	cfg.Sanitize()

	ex := executor.DefaultExecutor(cfg)
	ex.RebuildActionMap()
	ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
	defer ts.Close()

	_, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
		BindingId: "reboot-server",
		Arguments: []*apiv1.StartActionArgument{
			{Name: "target", Value: "not-a-real-server"},
		},
	}))
	require.Error(t, err)
	assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
}

func TestStartActionAllowsListedEntityArgumentValue(t *testing.T) {
	seedEntityArgumentGuessEntities(t)
	cfg := buildEntityArgumentGuessConfig()
	cfg.Entities[0].Acls = nil
	cfg.Sanitize()

	ex := executor.DefaultExecutor(cfg)
	ex.RebuildActionMap()
	ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
	defer ts.Close()

	resp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
		BindingId: "reboot-server",
		Arguments: []*apiv1.StartActionArgument{
			{Name: "target", Value: "web01"},
		},
	}))
	require.NoError(t, err)
	assert.NotEmpty(t, resp.Msg.ExecutionTrackingId)
}

func TestChecklistEntityValuesAllowedRejectsBlankOnlyInput(t *testing.T) {
	allowed := map[string]struct{}{"web01": {}, "db01": {}}
	arg := &config.ActionArgument{Type: "checklist"}

	assert.False(t, checklistEntityValuesAllowed(arg, ",,,", allowed))
	assert.False(t, checklistEntityValuesAllowed(arg, " , ", allowed))
	assert.False(t, checklistEntityValuesAllowed(arg, "", allowed),
		"all-blank checklist parts are rejected here; empty string is accepted by the caller separately")
	assert.True(t, checklistEntityValuesAllowed(arg, "web01", allowed))
	assert.True(t, checklistEntityValuesAllowed(arg, `["web01","db01"]`, allowed))
	assert.False(t, checklistEntityValuesAllowed(arg, `["web01","unknown"]`, allowed))
}

func TestChecklistEntityValuesAllowedAcceptsJSONArrayWithEntityTitles(t *testing.T) {
	entities.ClearEntitiesOfType("servers")
	t.Cleanup(func() {
		entities.ClearEntitiesOfType("servers")
	})
	entities.AddEntity("servers", "0", map[string]any{"name": "web01", "label": "Web Server One"})
	entities.AddEntity("servers", "1", map[string]any{"name": "db01", "label": "Database One"})

	arg := &config.ActionArgument{
		Type:   "checklist",
		Entity: "servers",
		Choices: []config.ActionArgumentChoice{
			{Title: "{{ servers.label }}", Value: "{{ servers.name }}"},
		},
	}
	allowed := entityArgumentAllowedValues(arg)

	assert.True(t, checklistEntityValuesAllowed(arg, `["Web Server One","Database One"]`, allowed))
	assert.False(t, checklistEntityValuesAllowed(arg, `["Web Server One","unknown"]`, allowed))
}

func TestEntityArgumentValueAllowedAcceptsEntityChoiceTitle(t *testing.T) {
	entities.ClearEntitiesOfType("servers")
	t.Cleanup(func() {
		entities.ClearEntitiesOfType("servers")
	})
	entities.AddEntity("servers", "0", map[string]any{"name": "web01", "label": "Web Server One"})

	arg := &config.ActionArgument{
		Entity: "servers",
		Choices: []config.ActionArgumentChoice{
			{Title: "{{ servers.label }}", Value: "{{ servers.name }}"},
		},
	}

	assert.True(t, entityArgumentValueAllowed(arg, "Web Server One"))
	assert.True(t, entityArgumentValueAllowed(arg, "web01"))
	assert.False(t, entityArgumentValueAllowed(arg, "unknown"))
}

func TestStartActionRejectsMalformedMultiChoiceEntityArgument(t *testing.T) {
	seedEntityArgumentGuessEntities(t)
	cfg := buildEntityArgumentGuessConfig()
	// After sanitize, force an invalid entity+multi-choice shape that would
	// previously skip ACL and fall through to static UI choices.
	cfg.Actions[0].Arguments[0].Choices = []config.ActionArgumentChoice{
		{Title: "{{ servers.name }}", Value: "{{ servers.name }}"},
		{Title: "web01", Value: "web01"},
	}

	ex := executor.DefaultExecutor(cfg)
	ex.RebuildActionMap()
	ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
	defer ts.Close()

	_, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
		BindingId: "reboot-server",
		Arguments: []*apiv1.StartActionArgument{
			{Name: "target", Value: "web01"},
		},
	}))
	require.Error(t, err)
	assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
}
