package dashboard

import (
	"bytes"
	"encoding/json"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

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

	"github.com/tidbcloud/AgentTeam/desktop/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
)

func TestBuildSkillResponse_OmitsEnabledFieldFromJSON(t *testing.T) {
	now := time.Now().UTC()
	resp := buildSkillResponse(profile.Skill{
		ID:          "skill-1",
		Name:        "Skill One",
		Description: "Test skill",
		Tags:        []string{"alpha"},
		FileName:    "skill-1/SKILL.md",
		CreatedAt:   &now,
	})

	body, err := json.Marshal(resp)
	require.NoError(t, err)
	require.NotContains(t, string(body), `"enabled"`)
	require.Contains(t, string(body), `"id":"skill-1"`)
	require.Contains(t, string(body), `"fileName":"skill-1/SKILL.md"`)
}

func setupSkillFilesRouter(t *testing.T) *gin.Engine {
	t.Helper()

	dir := t.TempDir()
	t.Setenv(profile.ConfigDirEnvKey, dir)

	mgr := profile.Default()
	require.NoError(t, mgr.Skills().Add(profile.Skill{
		ID:       "tree-skill",
		Name:     "Tree Skill",
		FileName: "tree-skill/SKILL.md",
	}))

	skillDir := filepath.Join(mgr.Skills().Dir(), "tree-skill")
	require.NoError(t, os.MkdirAll(filepath.Join(skillDir, "docs"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# Tree Skill\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(skillDir, "docs", "guide.md"), []byte("Guide\n"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(skillDir, "binary.bin"), []byte{0xff, 0xfe, 0xfd}, 0o644))

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.GET("/skills/:id/files/tree", NewSkillFilesTreeHandler())
	router.GET("/skills/:id/files/file", NewSkillFileHandler())
	return router
}

func TestSkillFilesTreeAndFileHandlers(t *testing.T) {
	router := setupSkillFilesRouter(t)

	treeReq := httptest.NewRequest(http.MethodGet, "/skills/tree-skill/files/tree", nil)
	treeRec := httptest.NewRecorder()
	router.ServeHTTP(treeRec, treeReq)

	require.Equal(t, http.StatusOK, treeRec.Code, treeRec.Body.String())
	var tree dto.SkillFilesTreeResponse
	require.NoError(t, json.Unmarshal(treeRec.Body.Bytes(), &tree))
	require.Equal(t, "~/.agentteam/skills/tree-skill", tree.Root)
	require.NotEmpty(t, tree.Nodes)
	require.Equal(t, "docs", tree.Nodes[0].Name)
	require.True(t, tree.Nodes[0].IsDir)

	fileReq := httptest.NewRequest(http.MethodGet, "/skills/tree-skill/files/file?path=docs%2Fguide.md", nil)
	fileRec := httptest.NewRecorder()
	router.ServeHTTP(fileRec, fileReq)

	require.Equal(t, http.StatusOK, fileRec.Code, fileRec.Body.String())
	var file dto.SkillFileResponse
	require.NoError(t, json.Unmarshal(fileRec.Body.Bytes(), &file))
	require.Equal(t, "docs/guide.md", file.Path)
	require.Equal(t, "Guide\n", file.Content)
	require.False(t, file.Binary)
}

func TestSkillFileRejectsTraversal(t *testing.T) {
	router := setupSkillFilesRouter(t)

	req := httptest.NewRequest(http.MethodGet, "/skills/tree-skill/files/file?path=..%2Fskills.yaml", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusBadRequest, rec.Code, rec.Body.String())
}

func TestSkillFilesMissingSkill(t *testing.T) {
	router := setupSkillFilesRouter(t)

	req := httptest.NewRequest(http.MethodGet, "/skills/missing/files/tree", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusNotFound, rec.Code, rec.Body.String())
}

func TestSkillFileBinaryPreview(t *testing.T) {
	router := setupSkillFilesRouter(t)

	req := httptest.NewRequest(http.MethodGet, "/skills/tree-skill/files/file?path=binary.bin", nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
	var file dto.SkillFileResponse
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &file))
	require.True(t, file.Binary)
	require.Empty(t, file.Content)
}

func TestImportSkillUploadTooLarge(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	restoreSkillImportUploadLimit(t, 64)

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.POST("/skills/import", NewImportSkillHandler())

	body, contentType := newSkillImportMultipart(t, "skill.zip", bytes.Repeat([]byte("x"), 256))
	req := httptest.NewRequest(http.MethodPost, "/skills/import", body)
	req.Header.Set("Content-Type", contentType)
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusRequestEntityTooLarge, rec.Code, rec.Body.String())
}

func TestImportBatchSkillUploadTooLarge(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())
	restoreSkillImportUploadLimit(t, 64)

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.POST("/skills/import-batch", NewImportBatchSkillHandler())

	body, contentType := newSkillImportMultipart(t, "skills.zip", bytes.Repeat([]byte("x"), 256))
	req := httptest.NewRequest(http.MethodPost, "/skills/import-batch", body)
	req.Header.Set("Content-Type", contentType)
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusRequestEntityTooLarge, rec.Code, rec.Body.String())
}

func TestImportSkillMultipleMarkdownFiles(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, t.TempDir())

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.POST("/skills/import", NewImportSkillHandler())

	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	part, err := writer.CreateFormFile("file", "alpha-skill.md")
	require.NoError(t, err)
	_, err = part.Write([]byte("# Alpha Skill\n\nAlpha description.\n"))
	require.NoError(t, err)
	part, err = writer.CreateFormFile("file", "beta-skill.md")
	require.NoError(t, err)
	_, err = part.Write([]byte("# Beta Skill\n\nBeta description.\n"))
	require.NoError(t, err)
	require.NoError(t, writer.Close())

	req := httptest.NewRequest(http.MethodPost, "/skills/import", &body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String())
	var resp dto.BatchImportResponse
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
	require.Equal(t, 2, resp.Created)
	require.Len(t, resp.Imported, 2)
	require.Empty(t, resp.Errors)

	alpha, ok := profile.Default().Skills().Get("alpha-skill")
	require.True(t, ok)
	require.Equal(t, "Alpha Skill", alpha.Name)
	require.Equal(t, "Alpha description.", alpha.Description)
	beta, ok := profile.Default().Skills().Get("beta-skill")
	require.True(t, ok)
	require.Equal(t, "Beta Skill", beta.Name)
}

func restoreSkillImportUploadLimit(t *testing.T, limit int64) {
	t.Helper()
	previous := skillImportUploadMaxBytes
	skillImportUploadMaxBytes = limit
	t.Cleanup(func() { skillImportUploadMaxBytes = previous })
}

func newSkillImportMultipart(t *testing.T, filename string, data []byte) (*bytes.Buffer, string) {
	t.Helper()
	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	part, err := writer.CreateFormFile("file", filename)
	require.NoError(t, err)
	_, err = part.Write(data)
	require.NoError(t, err)
	require.NoError(t, writer.Close())
	return &body, writer.FormDataContentType()
}
