package exec

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

func TestSkillRemoteUsesFiveRuntimeRoutesAndStreamsImports(t *testing.T) {
	workDir := t.TempDir()
	archive := filepath.Join(workDir, "skills.zip")
	require.NoError(t, os.WriteFile(archive, []byte("archive payload"), 0o600))

	var requests []string
	var jsonBodies []string
	var imports []map[string]string
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		requests = append(requests, request.Method+" "+request.URL.RequestURI())
		require.Equal(t, "Bearer token", request.Header.Get("Authorization"))
		if request.URL.Path == "/api/v1/workspaces/ws/agents/agent/runtime/skills/delete" {
			require.Equal(t, "req_delete_1", request.Header.Get("Idempotency-Key"))
			require.Equal(t, "req_delete_1", request.Header.Get("X-Request-ID"))
		}
		switch request.URL.Path {
		case "/api/v1/workspaces/ws/agents/agent/runtime/skills":
			_, _ = io.WriteString(writer, `{"items":[],"page":2,"perPage":7,"total":0,"hasMore":false}`)
		case "/api/v1/workspaces/ws/agents/agent/runtime/skills/get",
			"/api/v1/workspaces/ws/agents/agent/runtime/skills/create",
			"/api/v1/workspaces/ws/agents/agent/runtime/skills/update":
			body, err := io.ReadAll(request.Body)
			require.NoError(t, err)
			jsonBodies = append(jsonBodies, string(body))
			_, _ = io.WriteString(writer, `{"id":"skill_1","name":"Release","repositories":{}}`)
		case "/api/v1/workspaces/ws/agents/agent/runtime/skills/import":
			require.Equal(t, int64(-1), request.ContentLength, "multipart import must be streamed")
			require.Equal(t, []string{"chunked"}, request.TransferEncoding)
			require.Equal(t, "skill_1", request.URL.Query().Get("skillId"))
			reader, err := request.MultipartReader()
			require.NoError(t, err)
			got := map[string]string{}
			for {
				part, nextErr := reader.NextPart()
				if nextErr == io.EOF {
					break
				}
				require.NoError(t, nextErr)
				payload, readErr := io.ReadAll(part)
				require.NoError(t, readErr)
				got[part.FormName()] = string(payload)
				if part.FileName() != "" {
					got["filename"] = part.FileName()
				}
			}
			imports = append(imports, got)
			_, _ = io.WriteString(writer, `{"skill":{"id":"skill_1","name":"Release","repositories":{}},"importedSkills":[],"skillCount":1}`)
		case "/api/v1/workspaces/ws/agents/agent/runtime/skills/delete":
			body, err := io.ReadAll(request.Body)
			require.NoError(t, err)
			jsonBodies = append(jsonBodies, string(body))
			_, _ = io.WriteString(writer, `{"approvalId":"appr_skill","status":"pending"}`)
		default:
			t.Fatalf("unexpected request %s", request.URL.RequestURI())
		}
	}))
	t.Cleanup(server.Close)

	remote := skillRemote{
		client: &apiclient.Client{
			ServerURL: server.URL, AgentID: "agent", AuthToken: "token",
			RouteWorkspaceID: "ws", HTTPClient: server.Client(),
		},
		workDir: workDir,
	}
	_, err := remote.list(context.Background(), skillListRequest{
		Query: "release", Page: 2, PerPage: 7, TemplateLocale: "en-US", View: "created_by_me",
	})
	require.NoError(t, err)
	_, err = remote.get(context.Background(), skillGetRequest{SkillID: "skill_1"})
	require.NoError(t, err)
	_, err = remote.create(context.Background(), skillCreateRequest{
		Name: "Release", Description: "Release helper",
	})
	require.NoError(t, err)
	empty := ""
	_, err = remote.update(context.Background(), skillUpdateRequest{
		SkillID: "skill_1", Description: &empty,
	})
	require.NoError(t, err)
	_, err = remote.importContent(context.Background(), skillImportRequest{
		SkillID: "skill_1", URL: "https://example.test/skills.zip",
	})
	require.NoError(t, err)
	_, err = remote.importContent(context.Background(), skillImportRequest{
		SkillID: "skill_1", File: "skills.zip",
	})
	require.NoError(t, err)
	deleted, err := remote.delete(context.Background(), skillDeleteRequest{
		RequestID: "req_delete_1", SkillID: "skill_1",
	})
	require.NoError(t, err)
	require.Equal(t, skillDeleteResult{
		ApprovalID: "appr_skill", Status: "pending",
	}, deleted)

	require.Equal(t, []string{
		"GET /api/v1/workspaces/ws/agents/agent/runtime/skills?page=2&per_page=7&q=release&template_locale=en-US&view=created_by_me",
		"POST /api/v1/workspaces/ws/agents/agent/runtime/skills/get",
		"POST /api/v1/workspaces/ws/agents/agent/runtime/skills/create",
		"POST /api/v1/workspaces/ws/agents/agent/runtime/skills/update",
		"POST /api/v1/workspaces/ws/agents/agent/runtime/skills/import?skillId=skill_1",
		"POST /api/v1/workspaces/ws/agents/agent/runtime/skills/import?skillId=skill_1",
		"POST /api/v1/workspaces/ws/agents/agent/runtime/skills/delete",
	}, requests)
	require.JSONEq(t, `{"skillId":"skill_1"}`, jsonBodies[0])
	require.JSONEq(t, `{"name":"Release","description":"Release helper"}`, jsonBodies[1])
	require.JSONEq(t, `{"skillId":"skill_1","description":""}`, jsonBodies[2])
	require.JSONEq(t, `{"requestId":"req_delete_1","skillId":"skill_1"}`, jsonBodies[3])
	require.Equal(t, []map[string]string{
		{"url": "https://example.test/skills.zip"},
		{"file": "archive payload", "filename": "skills.zip"},
	}, imports)
}

func TestSkillRemoteImportRejectsPathsOutsideTheWorkDirAndUnsafeFiles(t *testing.T) {
	workDir := t.TempDir()
	outside := filepath.Join(t.TempDir(), "outside.zip")
	require.NoError(t, os.WriteFile(outside, []byte("outside"), 0o600))
	require.NoError(t, os.Mkdir(filepath.Join(workDir, "directory"), 0o700))
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "target.zip"), []byte("target"), 0o600))
	require.NoError(t, os.Symlink("target.zip", filepath.Join(workDir, "link.zip")))
	tooLarge := filepath.Join(workDir, "too-large.zip")
	require.NoError(t, os.WriteFile(tooLarge, nil, 0o600))
	require.NoError(t, os.Truncate(tooLarge, maxSkillImportBytes+1))

	var requests int
	server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		requests++
	}))
	t.Cleanup(server.Close)
	client := &apiclient.Client{
		ServerURL: server.URL, AgentID: "agent", AuthToken: "token",
		RouteWorkspaceID: "ws", HTTPClient: server.Client(),
	}

	tests := []struct {
		name    string
		workDir string
		file    string
		code    ErrorCode
	}{
		{name: "missing workdir", file: "target.zip", code: CodePermissionDenied},
		{name: "absolute path", workDir: workDir, file: filepath.Join(workDir, "target.zip"), code: CodePermissionDenied},
		{name: "traversal", workDir: workDir, file: filepath.Join("..", filepath.Base(filepath.Dir(outside)), "outside.zip"), code: CodePermissionDenied},
		{name: "missing file", workDir: workDir, file: "missing.zip", code: CodeNotFound},
		{name: "symlink", workDir: workDir, file: "link.zip", code: CodePermissionDenied},
		{name: "directory", workDir: workDir, file: "directory", code: CodePermissionDenied},
		{name: "too large", workDir: workDir, file: "too-large.zip", code: CodeValidation},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := (skillRemote{client: client, workDir: test.workDir}).importContent(
				context.Background(),
				skillImportRequest{SkillID: "skill_1", File: test.file},
			)
			require.Error(t, err)
			require.Equal(t, test.code, classify(err).Code)
			require.NotContains(t, err.Error(), outside)
			require.NotContains(t, strings.ToLower(err.Error()), "target.zip")
		})
	}
	require.Zero(t, requests)
}

func TestSkillRemoteImportRejectsFinalComponentSymlinkSwap(t *testing.T) {
	workDir := t.TempDir()
	archive := filepath.Join(workDir, "skills.zip")
	target := filepath.Join(workDir, "target.zip")
	require.NoError(t, os.WriteFile(archive, []byte("archive"), 0o600))
	require.NoError(t, os.WriteFile(target, []byte("target"), 0o600))
	server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		t.Fatal("rejected symlink swap reached the server")
	}))
	t.Cleanup(server.Close)

	remote := skillRemote{
		client: &apiclient.Client{
			ServerURL: server.URL, AgentID: "agent", AuthToken: "token",
			RouteWorkspaceID: "ws", HTTPClient: server.Client(),
		},
		workDir: workDir,
		beforeSecureOpen: func(resolved string) {
			require.NoError(t, os.Remove(resolved))
			require.NoError(t, os.Symlink("target.zip", resolved))
		},
	}
	_, err := remote.importContent(context.Background(), skillImportRequest{
		SkillID: "skill_1", File: "skills.zip",
	})
	require.Error(t, err)
	require.Equal(t, CodePermissionDenied, classify(err).Code)
}
