package webagent

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/testkit/httputil"
	"github.com/tidbcloud/AgentTeam/internal/service/role/agents"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
)

type fakeRoleRealizer struct {
	result          agents.RealizationResult
	materialization agent.AgentRoleMaterialization
	err             error
	materializeErr  error
	last            agents.RealizeRoleForAgentInput
	lastMaterialize agents.RealizeRoleForAgentInput
}

func (f *fakeRoleRealizer) RealizeRoleForAgent(_ context.Context, input agents.RealizeRoleForAgentInput) (agents.RealizationResult, error) {
	f.last = input
	if f.err != nil {
		return agents.RealizationResult{}, f.err
	}
	return f.result, nil
}

func (f *fakeRoleRealizer) RoleMaterialization(_ context.Context, input agents.RealizeRoleForAgentInput) (agent.AgentRoleMaterialization, error) {
	f.lastMaterialize = input
	if f.materializeErr != nil {
		return agent.AgentRoleMaterialization{}, f.materializeErr
	}
	if f.materialization.RoleID == "" {
		f.materialization = agent.AgentRoleMaterialization{RoleID: f.result.RoleID, RoleVersionID: "rlv_1", ActiveDefinitionPath: "memory/system/identity.md", ManifestItems: []agent.AgentRoleMaterializationItem{{Type: "role_definition_asset", Name: "IDENTITY.md", Content: "# Role"}}}
	}
	return f.materialization, nil
}

func TestRoleAgentsCreateFromRole(t *testing.T) {
	t.Run("preset source keeps blank POST untouched and returns realization", func(t *testing.T) {
		fake := newFakeStore()
		fake.createdAgent = agent.AgentView{AgentID: "agt_role", MachineID: "mach_1"}
		fake.appAgents["agt_role"] = agent.AppAgent{ID: "agt_role", Name: "research-agent", DisplayName: "Research Agent", MachineID: "mach_1", SourceKind: "preset", SourceRef: "research-agent"}
		realtime := &fakeRealtime{}
		realizer := &fakeRoleRealizer{result: agents.RealizationResult{RoleID: "role_copy", AgentID: "agt_role", Items: []agents.RealizationItem{{ManifestItemType: "role_definition_asset", Status: "realized", Required: true}}}}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, Realtime: realtime, RoleRealizer: realizer})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents:from-role", map[string]any{
			"name": " Research Agent ", "machineId": "mach_1", "runtime": "codex", "model": "gpt-5.4", "reasoningEffort": "medium",
			"source":          map[string]any{"kind": "preset", "presetId": "research-agent", "locale": "en-US"},
			"roleDisplayName": "My Research Agent",
			"realization":     map[string]any{"skipOptional": true, "storageScope": "workspace", "templateLocale": "en-US"},
		})
		require.Equal(t, http.StatusOK, resp.Code)
		require.Equal(t, "preset", fake.lastCreate.Params.SourceKind)
		require.Equal(t, "research-agent", fake.lastCreate.Params.SourceRef)
		require.Equal(t, "research-agent", realizer.last.Source.PresetID)
		require.Equal(t, "usr_1", realizer.last.UserID)
		require.Equal(t, "My Research Agent", realizer.last.RoleDisplayName)
		require.True(t, realizer.last.Realization.SkipOptional)
		require.Equal(t, "workspace", realizer.last.Realization.StorageScope)
		require.Equal(t, "en-US", realizer.last.Realization.TemplateLocale)
		require.Equal(t, "wsp_1", realtime.createdWorkspace)

		var out dto.WebAgentCreateFromRoleResponse
		require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &out))
		require.Equal(t, "agt_role", out.Agent.ID)
		require.Equal(t, "preset", out.Source.Kind)
		require.Equal(t, "research-agent", out.Source.PresetID)
		require.False(t, out.Partial)
		require.Equal(t, "role_copy", out.Realization.RoleID)
		require.Equal(t, "role_definition_asset", out.Realization.Items[0].ManifestItemType)
	})

	t.Run("create from role rejects create-time permission mode", func(t *testing.T) {
		fake := newFakeStore()
		realizer := &fakeRoleRealizer{}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, RoleRealizer: realizer})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents:from-role", map[string]any{
			"name": " Research Agent ", "machineId": "mach_1", "runtime": "codex", "permissionMode": "agent_workspace_write",
			"source": map[string]any{"kind": "preset", "presetId": "research-agent"},
		})
		require.Equal(t, http.StatusBadRequest, resp.Code)
		require.Contains(t, resp.Body.String(), "permissionMode is not supported when creating agents")
		require.Empty(t, fake.lastCreate.UserID)
	})

	t.Run("create from role preserves agent owner quota payload", func(t *testing.T) {
		fake := newFakeStore()
		fake.createErr = plan.QuotaExceededError{
			OrganizationID: "org_1",
			PlanKey:        plan.PlanFree,
			PlanName:       "Free",
			Metric:         plan.MetricAgents,
			Limit:          5,
			Current:        5,
			Delta:          1,
			Scope:          plan.QuotaScopeAgentOwner,
		}
		realizer := &fakeRoleRealizer{}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, RoleRealizer: realizer})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents:from-role", map[string]any{
			"name": "Research Agent", "machineId": "mach_1",
			"source": map[string]any{"kind": "my-role", "roleId": "role_1", "versionId": "rlv_1"},
		})

		require.Equal(t, http.StatusForbidden, resp.Code)
		var body map[string]any
		require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &body))
		require.Equal(t, "quota_exceeded", body["code"])
		require.Equal(t, "agents", body["metric"])
		require.Equal(t, float64(5), body["limit"])
		require.Equal(t, "free", body["plan"])
		require.Equal(t, "Free", body["planName"])
		require.Equal(t, "agent_owner", body["quotaScope"])
	})

	t.Run("applies role to existing agent and returns refreshed binding", func(t *testing.T) {
		fake := newFakeStore()
		fake.appAgents["agt_existing"] = agent.AppAgent{ID: "agt_existing", Name: "code2", DisplayName: "code2", MachineID: "mach_1", CurrentRoleBinding: &agent.AgentRoleBindingSummary{BindingKind: "my_role", RoleID: "role_1", RoleVersionID: "rlv_1", ActiveDefinitionPath: "memory/system/identity.md"}}
		realizer := &fakeRoleRealizer{
			result:          agents.RealizationResult{RoleID: "role_1", AgentID: "agt_existing", Apply: true, Items: []agents.RealizationItem{{ManifestItemType: "role_definition_asset", Status: "realized", Required: true}}},
			materialization: agent.AgentRoleMaterialization{RoleID: "role_1", RoleVersionID: "rlv_1", ActiveDefinitionPath: "memory/system/identity.md", ManifestItems: []agent.AgentRoleMaterializationItem{{Type: "role_definition_asset", Name: "IDENTITY.md", Content: "# Role"}}},
		}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, RoleRealizer: realizer})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents/agt_existing/role:apply", map[string]any{
			"source":      map[string]any{"kind": "my-role", "roleId": "role_1", "versionId": "rlv_1"},
			"realization": map[string]any{"apply": true},
		})
		require.Equal(t, http.StatusOK, resp.Code)
		require.Equal(t, "agt_existing", realizer.last.AgentID)
		require.Equal(t, "role_1", realizer.last.Source.RoleID)
		require.Equal(t, "role_1", realizer.lastMaterialize.Source.RoleID)
		require.Equal(t, "agt_existing", fake.lastRoleMaterialization.AgentID)
		require.Equal(t, "role_1", fake.lastRoleMaterialization.RoleID)
		var out dto.WebAgentApplyRoleResponse
		require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &out))
		require.Equal(t, "agt_existing", out.Agent.ID)
		require.NotNil(t, out.Agent.CurrentRoleBinding)
		require.Equal(t, "my-role", out.Agent.CurrentRoleBinding.BindingKind)
	})

	t.Run("my-role source requires version id and role id", func(t *testing.T) {
		fake := newFakeStore()
		realizer := &fakeRoleRealizer{}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, RoleRealizer: realizer})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents:from-role", map[string]any{
			"name": "Research Agent", "machineId": "mach_1",
			"source": map[string]any{"kind": "my-role", "roleId": "role_1", "versionId": ""},
		})
		require.Equal(t, http.StatusBadRequest, resp.Code)
		require.Contains(t, resp.Body.String(), "source.versionId")
	})

	t.Run("required realization failure returns conflict and cleanup detail", func(t *testing.T) {
		fake := newFakeStore()
		fake.createdAgent = agent.AgentView{AgentID: "agt_role", MachineID: "mach_1"}
		fake.deleteErr = assertErr("cleanup failed")
		realizer := &fakeRoleRealizer{result: agents.RealizationResult{RoleID: "role_copy", AgentID: "agt_role", Items: []agents.RealizationItem{{ManifestItemType: "knowledge_base", Status: "unresolved", Required: true}}}}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, RoleRealizer: realizer})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents:from-role", map[string]any{
			"name": "Research Agent", "machineId": "mach_1", "runtime": "codex", "model": "gpt-5.4", "reasoningEffort": "medium",
			"source": map[string]any{"kind": "preset", "presetId": "research-agent"},
		})
		require.Equal(t, http.StatusConflict, resp.Code)
		require.Contains(t, resp.Body.String(), "cleanup failed")
		require.Equal(t, "agt_role", fake.lastDeleteID)
	})

	t.Run("maps meshx invalid argument detail to readable apply error", func(t *testing.T) {
		fake := newFakeStore()
		realizer := &fakeRoleRealizer{err: errors.New(`meshx role realization failed: status 400: {"code":"invalid_argument","message":"target agent is missing"}`)}
		router := newWebAgentRouter(Dependencies{AgentStore: fake, ReviewStore: fake, RoleRealizer: realizer})

		resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/agents/agt_existing/role:apply", map[string]any{
			"source":      map[string]any{"kind": "my-role", "roleId": "role_1", "versionId": "rlv_1"},
			"realization": map[string]any{"apply": true},
		})
		require.Equal(t, http.StatusBadRequest, resp.Code)
		require.Contains(t, resp.Body.String(), "target agent is missing")
		require.NotContains(t, resp.Body.String(), "invalid_argument")
	})
}

func TestRoleAgentHTTPErrorLetsQuotaUseStoreEnvelope(t *testing.T) {
	quotaErr := plan.QuotaExceededError{
		OrganizationID: "org_1",
		PlanKey:        plan.PlanFree,
		PlanName:       "Free",
		Metric:         plan.MetricManagedMemories,
		Limit:          5,
		Current:        5,
		Delta:          1,
		Scope:          plan.QuotaScopeWorkspaceMember,
	}

	response, ok := roleAgentHTTPError(quotaErr)
	require.False(t, ok)
	require.Nil(t, response)

	response, ok = roleAgentHTTPError(&agents.RealizationError{Cause: quotaErr})
	require.False(t, ok)
	require.Nil(t, response)
}

func TestRoleAgentMeshXHTTPMessageDoesNotExposeBareInternalCode(t *testing.T) {
	message := roleAgentMeshXHTTPMessage(`meshx role realization failed: status 400: {"code":"invalid_argument"}`, http.StatusBadRequest)
	require.Equal(t, "role apply request is invalid", message)
}

type assertErr string

func (e assertErr) Error() string { return string(e) }
