package agent

import (
	"bytes"
	"context"
	"errors"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/service/approvalapp"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

type fakeAgentSkillOwners struct {
	workspaceID string
	agentID     string
	ownerID     string
	err         error
}

func (fake *fakeAgentSkillOwners) AgentManagedByUser(
	_ context.Context,
	workspaceID string,
	agentID string,
) (string, error) {
	fake.workspaceID, fake.agentID = workspaceID, agentID
	return fake.ownerID, fake.err
}

type fakeAgentSkillWorkspaces struct {
	userID      string
	workspaceID string
	summary     workspace.Summary
	err         error
}

func (fake *fakeAgentSkillWorkspaces) GetWorkspaceForUser(
	_ context.Context,
	userID string,
	workspaceID string,
) (workspace.Summary, error) {
	fake.userID, fake.workspaceID = userID, workspaceID
	return fake.summary, fake.err
}

type fakeAgentSkillGateway struct {
	principals  []AgentSkillPrincipal
	listInput   AgentSkillListInput
	getID       string
	createInput dto.SkillCreateRequest
	updateID    string
	updateInput AgentSkillUpdateInput
	importID    string
	importType  string
	importForm  map[string]string
	deleteID    string
	deleteCalls int

	listResult   dto.SkillListResponse
	skillResult  dto.Skill
	importResult dto.SkillImportResponse
	err          error
	getErr       error
	deleteErr    error
}

func (fake *fakeAgentSkillGateway) List(
	_ context.Context,
	principal AgentSkillPrincipal,
	input AgentSkillListInput,
) (dto.SkillListResponse, error) {
	fake.principals = append(fake.principals, principal)
	fake.listInput = input
	return fake.listResult, fake.err
}

func (fake *fakeAgentSkillGateway) Get(
	_ context.Context,
	principal AgentSkillPrincipal,
	skillID string,
) (dto.Skill, error) {
	fake.principals = append(fake.principals, principal)
	fake.getID = skillID
	if fake.getErr != nil {
		return dto.Skill{}, fake.getErr
	}
	return fake.skillResult, fake.err
}

func (fake *fakeAgentSkillGateway) Create(
	_ context.Context,
	principal AgentSkillPrincipal,
	input dto.SkillCreateRequest,
) (dto.Skill, error) {
	fake.principals = append(fake.principals, principal)
	fake.createInput = input
	return fake.skillResult, fake.err
}

func (fake *fakeAgentSkillGateway) Update(
	_ context.Context,
	principal AgentSkillPrincipal,
	skillID string,
	input AgentSkillUpdateInput,
) (dto.Skill, error) {
	fake.principals = append(fake.principals, principal)
	fake.updateID, fake.updateInput = skillID, input
	return fake.skillResult, fake.err
}

func (fake *fakeAgentSkillGateway) Import(
	_ context.Context,
	principal AgentSkillPrincipal,
	skillID string,
	contentType string,
	body io.Reader,
) (dto.SkillImportResponse, error) {
	fake.principals = append(fake.principals, principal)
	fake.importID, fake.importType = skillID, contentType
	_, parameters, err := mime.ParseMediaType(contentType)
	if err != nil {
		return dto.SkillImportResponse{}, err
	}
	reader := multipart.NewReader(body, parameters["boundary"])
	fake.importForm = map[string]string{}
	for {
		part, nextErr := reader.NextPart()
		if nextErr == io.EOF {
			break
		}
		if nextErr != nil {
			return dto.SkillImportResponse{}, nextErr
		}
		payload, readErr := io.ReadAll(part)
		if readErr != nil {
			return dto.SkillImportResponse{}, readErr
		}
		fake.importForm[part.FormName()] = string(payload)
	}
	return fake.importResult, fake.err
}

func (fake *fakeAgentSkillGateway) Delete(
	_ context.Context,
	principal AgentSkillPrincipal,
	skillID string,
) error {
	fake.principals = append(fake.principals, principal)
	fake.deleteID = skillID
	fake.deleteCalls++
	if fake.deleteErr != nil {
		return fake.deleteErr
	}
	return fake.err
}

type fakeAgentSkillApprovals struct {
	requestType string
	command     approvalapp.SubmitCommand
	result      approvalapp.SubmitResult
	err         error
	presented   []approvalapp.SubmitResult
}

func (fake *fakeAgentSkillApprovals) SubmitRequest(
	_ context.Context,
	requestType string,
	command approvalapp.SubmitCommand,
) (approvalapp.SubmitResult, error) {
	fake.requestType, fake.command = requestType, command
	return fake.result, fake.err
}

func (fake *fakeAgentSkillApprovals) PresentPending(
	_ context.Context,
	result approvalapp.SubmitResult,
) (app.AppNotificationResult, error) {
	fake.presented = append(fake.presented, result)
	return app.AppNotificationResult{}, nil
}

func agentSkillTestRouter(
	owners AgentSkillOwnerResolver,
	skills AgentSkillGateway,
) *gin.Engine {
	return agentSkillTestRouterWithWorkspace(
		owners,
		&fakeAgentSkillWorkspaces{summary: workspace.Summary{
			WorkspaceID: "wsp_test", MemberID: "mem_human",
			MemberUserID: "usr_human", WorkspaceRole: "member",
		}},
		skills,
	)
}

func agentSkillTestRouterWithWorkspace(
	owners AgentSkillOwnerResolver,
	workspaces AgentSkillWorkspaceResolver,
	skills AgentSkillGateway,
) *gin.Engine {
	return agentSkillTestRouterWithApproval(
		owners, workspaces, skills, nil,
	)
}

func agentSkillTestRouterWithApproval(
	owners AgentSkillOwnerResolver,
	workspaces AgentSkillWorkspaceResolver,
	skills AgentSkillGateway,
	approvals ApprovalRuntime,
) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	RegisterRoutes(router, Dependencies{
		Auth: fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{
			AgentID: "agt_0000000000test", WorkspaceID: "wsp_test",
			MemberID: "00000000test", MachineID: "mac_test",
		}},
		SkillOwners:      owners,
		SkillWorkspace:   workspaces,
		Skills:           skills,
		ApprovalSubjects: approvals,
	})
	return router
}

func TestAgentSkillRoutesGateDeleteOnApprovalRuntime(t *testing.T) {
	router := agentSkillTestRouter(&fakeAgentSkillOwners{}, &fakeAgentSkillGateway{})
	routes := map[string]bool{}
	for _, route := range router.Routes() {
		routes[route.Method+" "+route.Path] = true
	}
	base := "/api/v1/workspaces/:workspaceId/agents/:agentId/runtime/skills"
	for _, route := range []string{
		http.MethodGet + " " + base,
		http.MethodPost + " " + base + "/get",
		http.MethodPost + " " + base + "/create",
		http.MethodPost + " " + base + "/update",
		http.MethodPost + " " + base + "/import",
	} {
		require.True(t, routes[route], route)
	}
	require.False(t, routes[http.MethodPost+" "+base+"/delete"],
		"delete must fail closed without the Approval runtime")

	router = agentSkillTestRouterWithApproval(
		&fakeAgentSkillOwners{},
		&fakeAgentSkillWorkspaces{},
		&fakeAgentSkillGateway{},
		&fakeAgentSkillApprovals{},
	)
	routes = map[string]bool{}
	for _, route := range router.Routes() {
		routes[route.Method+" "+route.Path] = true
	}
	require.True(t, routes[http.MethodPost+" "+base+"/delete"])
}

func TestAgentSkillRoutesRequireOwnerWorkspaceAndGateway(t *testing.T) {
	for name, dependencies := range map[string]Dependencies{
		"owner": {
			SkillWorkspace: &fakeAgentSkillWorkspaces{},
			Skills:         &fakeAgentSkillGateway{},
		},
		"workspace": {
			SkillOwners: &fakeAgentSkillOwners{},
			Skills:      &fakeAgentSkillGateway{},
		},
		"gateway": {
			SkillOwners:    &fakeAgentSkillOwners{},
			SkillWorkspace: &fakeAgentSkillWorkspaces{},
		},
	} {
		t.Run(name, func(t *testing.T) {
			dependencies.Auth = fakeAgentAuthenticator{agent: storeagent.AuthenticatedAgent{
				AgentID: "agt_0000000000test", WorkspaceID: "wsp_test",
			}}
			gin.SetMode(gin.TestMode)
			router := gin.New()
			RegisterRoutes(router, dependencies)
			for _, route := range router.Routes() {
				require.NotContains(t, route.Path, "/skills")
			}
		})
	}
}

func TestAgentSkillDeleteSubmitsServerAuthenticatedApproval(t *testing.T) {
	approvals := &fakeAgentSkillApprovals{result: approvalapp.SubmitResult{
		Instance: approval.Instance{
			ID: "appr_skill", State: approval.StatePending,
		},
		Value: approvalapp.SkillDeleteResult{
			ApprovalID: "appr_skill", Status: approvalapp.SkillDeletePending,
		},
	}}
	router := agentSkillTestRouterWithApproval(
		&fakeAgentSkillOwners{},
		&fakeAgentSkillWorkspaces{},
		&fakeAgentSkillGateway{},
		approvals,
	)

	response := agentTaskRequest(t, router, http.MethodPost, "/skills/delete", map[string]any{
		"requestId": "req_delete_1",
		"skillId":   "skill_1",
	})

	require.Equal(t, http.StatusOK, response.Code, response.Body.String())
	require.JSONEq(t, `{"approvalId":"appr_skill","status":"pending"}`, response.Body.String())
	require.Equal(t, approvalapp.SkillDeleteRequestType, approvals.requestType)
	require.Equal(t, approvalapp.SubjectPrincipal{
		ActorType: "agent", WorkspaceID: "wsp_test",
		PrincipalID: "agt_0000000000test",
		MemberID:    "00000000test", MachineID: "mac_test",
	}, approvals.command.Principal)
	require.Equal(t, "req_delete_1", approvals.command.ClientRequestID)
	require.Equal(t, approvalapp.SkillDeleteRequest{SkillID: "skill_1"}, approvals.command.Payload)
	require.Len(t, approvals.presented, 1)
}

func TestAgentSkillOperationsRejectInactiveHumanOwnerBeforeGateway(t *testing.T) {
	owners := &fakeAgentSkillOwners{ownerID: "usr_offboarded"}
	workspaces := &fakeAgentSkillWorkspaces{err: storeerr.ErrForbidden}
	gateway := &fakeAgentSkillGateway{}
	router := agentSkillTestRouterWithWorkspace(owners, workspaces, gateway)

	response := agentTaskRequest(t, router, http.MethodGet, "/skills", nil)

	require.Equal(t, http.StatusForbidden, response.Code, response.Body.String())
	require.Equal(t, "usr_offboarded", workspaces.userID)
	require.Equal(t, "wsp_test", workspaces.workspaceID)
	require.Empty(t, gateway.principals, "inactive Human owner must not reach the trusted Skill gateway")
}

func TestAgentSkillOperationsUseOnlyCurrentHumanOwner(t *testing.T) {
	active := true
	name := "Renamed"
	description := ""
	owners := &fakeAgentSkillOwners{ownerID: "usr_human"}
	workspaces := &fakeAgentSkillWorkspaces{summary: workspace.Summary{
		WorkspaceID: "wsp_test", MemberID: "mem_human",
		MemberUserID: "usr_human", WorkspaceRole: "member",
	}}
	gateway := &fakeAgentSkillGateway{
		listResult: dto.SkillListResponse{
			Items: []dto.Skill{{ID: "skill_1", Name: "Release", Repositories: dto.SkillRepositories{}}},
			Page:  2, PerPage: 7,
		},
		skillResult: dto.Skill{ID: "skill_1", Name: "Release", Repositories: dto.SkillRepositories{}},
		importResult: dto.SkillImportResponse{
			Skill:          dto.Skill{ID: "skill_1", Name: "Release", Repositories: dto.SkillRepositories{}},
			ImportedSkills: []dto.SkillRepositoryDetectedSkill{},
			SkillCount:     1,
		},
	}
	router := agentSkillTestRouterWithWorkspace(owners, workspaces, gateway)

	list := agentTaskRequest(t, router, http.MethodGet,
		"/skills?q=release&page=2&per_page=7&template_locale=en-US&view=created_by_me", nil)
	require.Equal(t, http.StatusOK, list.Code, list.Body.String())
	require.Equal(t, AgentSkillListInput{
		Query: "release", Page: 2, PerPage: 7, TemplateLocale: "en-US", View: "created_by_me",
	}, gateway.listInput)

	get := agentTaskRequest(t, router, http.MethodPost, "/skills/get", map[string]any{"skillId": "skill_1"})
	require.Equal(t, http.StatusOK, get.Code, get.Body.String())
	require.Equal(t, "skill_1", gateway.getID)

	create := agentTaskRequest(t, router, http.MethodPost, "/skills/create", map[string]any{
		"name": "Release", "description": "Release helper", "repoNameOverride": "release",
		"storageScope": "personal", "visibilityScope": "private",
		"templateLocale": "en-US", "isActive": true,
	})
	require.Equal(t, http.StatusOK, create.Code, create.Body.String())
	require.Equal(t, dto.SkillCreateRequest{
		Name: "Release", Description: "Release helper", RepoNameOverride: "release",
		StorageScope: "personal", VisibilityScope: "private",
		TemplateLocale: "en-US", IsActive: &active,
	}, gateway.createInput)

	update := agentTaskRequest(t, router, http.MethodPost, "/skills/update", map[string]any{
		"skillId": "skill_1", "name": "Renamed", "description": "",
	})
	require.Equal(t, http.StatusOK, update.Code, update.Body.String())
	require.Equal(t, "skill_1", gateway.updateID)
	require.Equal(t, AgentSkillUpdateInput{Name: &name, Description: &description}, gateway.updateInput)

	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	require.NoError(t, writer.WriteField("url", "https://example.test/skills.zip"))
	require.NoError(t, writer.Close())
	importRequest := httptest.NewRequest(http.MethodPost,
		"/api/v1/workspaces/wsp_test/agents/agt_0000000000test/runtime/skills/import?skillId=skill_1",
		&body)
	importRequest.Header.Set("Authorization", "Bearer test-token")
	importRequest.Header.Set("Content-Type", writer.FormDataContentType())
	importResponse := httptest.NewRecorder()
	router.ServeHTTP(importResponse, importRequest)
	require.Equal(t, http.StatusOK, importResponse.Code, importResponse.Body.String())
	require.Equal(t, "skill_1", gateway.importID)
	require.Equal(t, map[string]string{"url": "https://example.test/skills.zip"}, gateway.importForm)

	require.Len(t, gateway.principals, 5)
	for _, principal := range gateway.principals {
		require.Equal(t, AgentSkillPrincipal{
			WorkspaceID: "wsp_test", Subject: "usr_human",
		}, principal)
	}
	require.Equal(t, "wsp_test", owners.workspaceID)
	require.Equal(t, "agt_0000000000test", owners.agentID)
	require.Equal(t, "usr_human", workspaces.userID)
	require.Equal(t, "wsp_test", workspaces.workspaceID)
	for _, response := range []*httptest.ResponseRecorder{list, get, create, update, importResponse} {
		require.NotContains(t, response.Body.String(), "usr_human")
		require.NotContains(t, response.Body.String(), "subject")
	}
}

func TestAgentSkillJSONRejectsCallerIdentityAndRepositoryUpdate(t *testing.T) {
	router := agentSkillTestRouter(
		&fakeAgentSkillOwners{ownerID: "usr_human"},
		&fakeAgentSkillGateway{},
	)
	for _, test := range []struct {
		path string
		body map[string]any
	}{
		{path: "/skills/create", body: map[string]any{
			"name": "Release", "workspaceId": "wsp_other",
		}},
		{path: "/skills/update", body: map[string]any{
			"skillId": "skill_1", "repoName": "renamed",
		}},
	} {
		response := agentTaskRequest(t, router, http.MethodPost, test.path, test.body)
		require.Equal(t, http.StatusBadRequest, response.Code, response.Body.String())
	}
}

func TestAgentSkillDeleteAuthorityRechecksOwnerMembershipAndManage(t *testing.T) {
	owners := &fakeAgentSkillOwners{ownerID: "usr_owner"}
	workspaces := &fakeAgentSkillWorkspaces{summary: workspace.Summary{
		WorkspaceID: "wsp_1", MemberID: "mem_owner",
		MemberUserID: "usr_owner",
	}}
	gateway := &fakeAgentSkillGateway{skillResult: dto.Skill{
		ID: "skill_1", Name: "Release", CanManage: true,
	}}
	authority := NewAgentSkillDeleteAuthority(owners, workspaces, gateway)
	command := approvalapp.SkillDeleteAuthorityCommand{
		Principal: approvalapp.SubjectPrincipal{
			ActorType: "agent", WorkspaceID: "wsp_1",
			PrincipalID: "agt_1", MemberID: "mem_agent", MachineID: "mac_1",
		},
		SkillID: "skill_1",
	}

	prepared, err := authority.Prepare(context.Background(), command)
	require.NoError(t, err)
	require.Equal(t, approvalapp.SkillDeletePrepared{
		SkillID: "skill_1", SkillName: "Release",
		ApproverMemberID: "mem_owner",
	}, prepared)

	workspaces.err = storeerr.ErrForbidden
	err = authority.Execute(context.Background(), command)
	require.ErrorIs(t, err, storeerr.ErrForbidden)
	require.Zero(t, gateway.deleteCalls,
		"offboarded current owner must not reach Resource DELETE")

	workspaces.err = nil
	gateway.skillResult.CanManage = false
	err = authority.Execute(context.Background(), command)
	require.ErrorIs(t, err, storeerr.ErrForbidden)
	require.Zero(t, gateway.deleteCalls,
		"lost manage permission must not reach Resource DELETE")
}

func TestAgentSkillDeleteAuthorityTreatsApproved404ReplayAsDeleted(t *testing.T) {
	owners := &fakeAgentSkillOwners{ownerID: "usr_owner"}
	workspaces := &fakeAgentSkillWorkspaces{summary: workspace.Summary{
		WorkspaceID: "wsp_1", MemberID: "mem_owner",
		MemberUserID: "usr_owner",
	}}
	gateway := &fakeAgentSkillGateway{skillResult: dto.Skill{
		ID: "skill_1", Name: "Release", CanManage: true,
	}}
	authority := NewAgentSkillDeleteAuthority(owners, workspaces, gateway)
	command := approvalapp.SkillDeleteAuthorityCommand{
		Principal: approvalapp.SubjectPrincipal{
			ActorType: "agent", WorkspaceID: "wsp_1",
			PrincipalID: "agt_1", MemberID: "mem_agent", MachineID: "mac_1",
		},
		SkillID: "skill_1",
	}

	gateway.getErr = storeerr.ErrNotFound
	require.NoError(t, authority.Execute(context.Background(), command))
	require.Zero(t, gateway.deleteCalls)

	gateway.getErr = nil
	gateway.deleteErr = storeerr.ErrNotFound
	require.NoError(t, authority.Execute(context.Background(), command))
	require.Equal(t, 1, gateway.deleteCalls)

	gateway.deleteErr = errors.New("upstream unavailable")
	err := authority.Execute(context.Background(), command)
	require.ErrorContains(t, err, "upstream unavailable")
	require.Equal(t, 2, gateway.deleteCalls)
}
