package roles

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/identity/trusted"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	rolesstore "github.com/tidbcloud/AgentTeam/internal/store/resource/roles"
)

type testAgentOwnershipSource struct {
	owned map[string]bool
}

func (s *testAgentOwnershipSource) RequireOwnedAgent(_ context.Context, _, _, agentID string) error {
	if s != nil && s.owned[agentID] {
		return nil
	}
	return repos.ErrForbidden
}

func TestClaimsAgentOwnershipSourceFailsClosed(t *testing.T) {
	source := ClaimsAgentOwnershipSource{}

	for name, ctx := range map[string]context.Context{
		"missing claims": context.Background(),
		"subject mismatch": trusted.WithIdentity(context.Background(), trusted.Identity{Subject: "usr_other", RawClaims: map[string]any{
			"agent_ownership_workspace_id": "wsp_1",
			"owned_agent_ids":              []any{"agt_1"},
		}}),
		"workspace mismatch": trusted.WithIdentity(context.Background(), trusted.Identity{Subject: "usr_1", RawClaims: map[string]any{
			"agent_ownership_workspace_id": "wsp_other",
			"owned_agent_ids":              []any{"agt_1"},
		}}),
		"target absent": trusted.WithIdentity(context.Background(), trusted.Identity{Subject: "usr_1", RawClaims: map[string]any{
			"agent_ownership_workspace_id": "wsp_1",
			"owned_agent_ids":              []any{"agt_other"},
		}}),
	} {
		t.Run(name, func(t *testing.T) {
			err := source.RequireOwnedAgent(ctx, "wsp_1", "usr_1", "agt_1")
			require.ErrorIs(t, err, repos.ErrForbidden)
		})
	}

	ctx := trusted.WithIdentity(context.Background(), trusted.Identity{Subject: "usr_1", RawClaims: map[string]any{
		"agent_ownership_workspace_id": "wsp_1",
		"owned_agents":                 []any{map[string]any{"id": "agt_1"}},
	}})
	require.NoError(t, source.RequireOwnedAgent(ctx, "wsp_1", "usr_1", "agt_1"))
}

func TestRoleServiceDefaultsApplyOwnershipToVerifiedClaims(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name: "Reviewer", Definition: "review",
	})
	require.NoError(t, err)
	operation := MutationOperation{
		OperationKey: "claims-backed-apply", RequestDigest: digestForTest("claims-backed-apply"),
	}
	input := RealizationRequest{AgentID: "agt_1", RoleVersionID: role.RoleVersionID, Apply: true}

	_, err = svc.ApplyRoleOperation(context.Background(), "wsp_1", "usr_1", role.ID, operation, input)
	require.ErrorIs(t, err, repos.ErrForbidden)

	ctx := trusted.WithIdentity(context.Background(), trusted.Identity{Subject: "usr_1", RawClaims: map[string]any{
		"agent_ownership_workspace_id": "wsp_1",
		"owned_agent_ids":              []any{"agt_1"},
	}})
	_, err = svc.ApplyRoleOperation(ctx, "wsp_1", "usr_1", role.ID, operation, input)
	require.NoError(t, err)
}

func TestRoleOperationPreflightLeavesEveryRoleFactUnchanged(t *testing.T) {
	ownership := &testAgentOwnershipSource{owned: map[string]bool{"agt_1": true}}
	svc := newRoleServiceForTest(t, Options{Ownership: ownership})
	ctx := context.Background()
	role, err := svc.CreateRole(ctx, "wsp_1", "usr_1", Mutation{
		Name: "Reviewer", Definition: "review",
		Attachments: []Attachment{{
			Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "strict rubric",
		}},
	})
	require.NoError(t, err)
	before := roleFactSnapshotForTest(t, svc)

	require.NoError(t, svc.PreflightCreateRoleOperation(ctx, "wsp_1", "usr_1",
		Mutation{Name: "Writer", Definition: "write"}))
	name := "Senior reviewer"
	require.NoError(t, svc.PreflightUpdateRoleOperation(ctx, "wsp_1", "usr_1", role.ID,
		role.RoleVersionID, RoleUpdatePatch{Name: &name}))
	require.NoError(t, svc.PreflightDeleteRoleOperation(ctx, "wsp_1", "usr_1", role.ID, role.RoleVersionID))
	impact, err := svc.PreflightApplyRoleOperation(ctx, "wsp_1", "usr_1", role.ID,
		RealizationRequest{AgentID: "agt_1", RoleVersionID: role.RoleVersionID, Apply: true})
	require.NoError(t, err)
	require.Equal(t, RealizationImpact{}, impact)

	require.Equal(t, before, roleFactSnapshotForTest(t, svc))
}

func TestUpdatePreflightUsesOperationMutationValidation(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	role, err := svc.CreateRole(ctx, "wsp_1", "usr_1", Mutation{Name: "Reviewer", Definition: "review"})
	require.NoError(t, err)
	empty := ""

	err = svc.PreflightUpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID, role.RoleVersionID, RoleUpdatePatch{Name: &empty},
	)
	require.ErrorIs(t, err, repos.ErrInvalidArgument)

	_, err = svc.UpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "invalid-empty-name", RequestDigest: digestForTest("empty")},
		role.RoleVersionID, RoleUpdatePatch{Name: &empty},
	)
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
}

func TestUpdateAdmissionRejectsEmptyPatchWithoutSideEffectsAndAllowsExplicitDescriptionClear(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	role, err := svc.CreateRole(ctx, "wsp_1", "usr_1", Mutation{
		Name: "Reviewer", Description: "clear me", Definition: "review",
	})
	require.NoError(t, err)
	before := roleFactSnapshotForTest(t, svc)

	err = svc.PreflightUpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID, role.RoleVersionID, RoleUpdatePatch{},
	)
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	require.Equal(t, before, roleFactSnapshotForTest(t, svc))

	_, err = svc.UpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "empty-update", RequestDigest: digestForTest("empty-update")},
		role.RoleVersionID, RoleUpdatePatch{},
	)
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	require.Equal(t, before, roleFactSnapshotForTest(t, svc))

	empty := ""
	require.NoError(t, svc.PreflightUpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID, role.RoleVersionID,
		RoleUpdatePatch{Description: &empty},
	))
	updated, err := svc.UpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "clear-description", RequestDigest: digestForTest("clear-description")},
		role.RoleVersionID, RoleUpdatePatch{Description: &empty},
	)
	require.NoError(t, err)
	require.Empty(t, updated.Description)
}

type roleFactSnapshot struct {
	Roles            string
	Versions         string
	ManifestItems    string
	Grants           string
	Receipts         string
	Realizations     string
	RealizationItems string
	Bindings         string
	Content          map[string]string
}

func roleFactSnapshotForTest(t *testing.T, svc *Service) roleFactSnapshot {
	t.Helper()
	return roleFactSnapshot{
		Roles:            tableJSONForTest[rolesstore.Role](t, svc, "id"),
		Versions:         tableJSONForTest[rolesstore.RoleVersion](t, svc, "id"),
		ManifestItems:    tableJSONForTest[rolesstore.RolePackageManifestItem](t, svc, "id"),
		Grants:           tableJSONForTest[rolesstore.RoleGrant](t, svc, "id"),
		Receipts:         tableJSONForTest[rolesstore.RoleOperationReceipt](t, svc, "operation_key"),
		Realizations:     tableJSONForTest[rolesstore.RoleRealization](t, svc, "id"),
		RealizationItems: tableJSONForTest[rolesstore.RoleRealizationItem](t, svc, "id"),
		Bindings:         tableJSONForTest[rolesstore.AgentRoleBinding](t, svc, "id"),
		Content:          cloneRolePackageContents(svc.contentStore.(*testRolePackageContentStore).contents),
	}
}

func tableJSONForTest[T any](t *testing.T, svc *Service, order string) string {
	t.Helper()
	var rows []T
	require.NoError(t, svc.store.DB().Order(order).Find(&rows).Error)
	data, err := json.Marshal(rows)
	require.NoError(t, err)
	return string(data)
}
