package projects

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	"senlinai-agent/backend/internal/config"
	"senlinai-agent/backend/internal/httpx"
	"senlinai-agent/backend/internal/models"
)

func TestCreateProjectHandlerPersistsMetadata(t *testing.T) {
	router, _, _ := newProjectsHandlerTestRouter(t)
	body, err := json.Marshal(gin.H{
		"name":        "Explore",
		"identifier":  "EXP",
		"icon":        "compass",
		"background":  "#165DFF",
		"description": "RSS exploration workspace",
	})
	require.NoError(t, err)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer test-token")
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusCreated, rec.Code)
	var project models.SenlinAgentProject
	require.NoError(t, models.DBService.Where("owner_id = ? AND identifier = ?", 1, "EXP").First(&project).Error)
	require.Equal(t, "Explore", project.Name)
	require.Equal(t, "compass", project.Icon)
	require.Equal(t, "#165DFF", project.Background)
	require.Equal(t, "RSS exploration workspace", project.Description)

	var payload map[string]any
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Equal(t, project.Identity, payload["id"])
	require.ElementsMatch(t, []string{"id", "name", "identifier", "icon", "background", "description"}, mapKeys(payload))
}

func TestCreateProjectReturnsChineseInvalidRequestEnvelope(t *testing.T) {
	router, _, _ := newProjectsHandlerTestRouter(t)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader([]byte(`{"name":" "}`)))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer test-token")
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusBadRequest, rec.Code)
	var payload httpx.ErrorEnvelope
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Equal(t, "invalid_request", payload.Error.Code)
	require.Equal(t, "请求参数无效", payload.Error.Message)
}

func TestCreateProjectReturnsConflictForDuplicateIdentifier(t *testing.T) {
	router, project, _ := newProjectsHandlerTestRouter(t)
	body, err := json.Marshal(gin.H{"name": "Another", "identifier": project.Identifier})
	require.NoError(t, err)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer test-token")
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusConflict, rec.Code)
	var payload httpx.ErrorEnvelope
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Equal(t, "conflict", payload.Error.Code)
	require.Equal(t, "项目标识已存在", payload.Error.Message)
}

func TestGetProjectUsesOwnedIdentityDTO(t *testing.T) {
	router, project, _ := newProjectsHandlerTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+project.Identity, nil)
	req.Header.Set("Authorization", "Bearer test-token")
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var payload map[string]any
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Equal(t, project.Identity, payload["id"])
	require.ElementsMatch(t, []string{"id", "name", "identifier", "icon", "background", "description"}, mapKeys(payload))
}

func TestListProjectsUsesIdentityDTO(t *testing.T) {
	router, project, _ := newProjectsHandlerTestRouter(t)
	req := httptest.NewRequest(http.MethodGet, "/api/v1/projects", nil)
	req.Header.Set("Authorization", "Bearer test-token")
	rec := httptest.NewRecorder()

	router.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var payload []map[string]any
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Len(t, payload, 1)
	require.Equal(t, project.Identity, payload[0]["id"])
	require.ElementsMatch(t, []string{"id", "name", "identifier", "icon", "background", "description"}, mapKeys(payload[0]))
}

func TestUpdateProject(t *testing.T) {
	t.Run("updates the allowed project fields by identity", func(t *testing.T) {
		router, project, _ := newProjectsHandlerTestRouter(t)
		body, err := json.Marshal(gin.H{
			"name":        "Alpha Next",
			"identifier":  "ALPHA-NEXT",
			"icon":        "tree",
			"background":  "#0FC6C2",
			"description": "更新后的项目说明",
		})
		require.NoError(t, err)
		req := httptest.NewRequest(http.MethodPatch, "/api/v1/projects/"+project.Identity, bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer test-token")
		rec := httptest.NewRecorder()

		router.ServeHTTP(rec, req)

		require.Equal(t, http.StatusOK, rec.Code)
		var payload map[string]any
		require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
		require.ElementsMatch(t, []string{"id", "name", "identifier", "icon", "background", "description"}, mapKeys(payload))
		require.Equal(t, project.Identity, payload["id"])
		require.Equal(t, "Alpha Next", payload["name"])
		require.Equal(t, "ALPHA-NEXT", payload["identifier"])
		require.Equal(t, "tree", payload["icon"])
		require.Equal(t, "#0FC6C2", payload["background"])
		require.Equal(t, "更新后的项目说明", payload["description"])
		var updated models.SenlinAgentProject
		require.NoError(t, models.DBService.First(&updated, project.ID).Error)
		require.Equal(t, "Alpha Next", updated.Name)
		require.Equal(t, "ALPHA-NEXT", updated.Identifier)
		require.Equal(t, "tree", updated.Icon)
		require.Equal(t, "#0FC6C2", updated.Background)
		require.Equal(t, "更新后的项目说明", updated.Description)
	})

	t.Run("clears optional metadata while preserving omitted name and identifier", func(t *testing.T) {
		router, project, _ := newProjectsHandlerTestRouter(t)
		require.NoError(t, models.DBService.Model(project).Updates(map[string]any{
			"icon":        "tree",
			"background":  "#165DFF",
			"description": "原项目说明",
		}).Error)
		originalName := project.Name
		originalIdentifier := project.Identifier
		body, err := json.Marshal(gin.H{
			"icon":        "",
			"background":  "",
			"description": "",
		})
		require.NoError(t, err)
		req := httptest.NewRequest(http.MethodPatch, "/api/v1/projects/"+project.Identity, bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer test-token")
		rec := httptest.NewRecorder()

		router.ServeHTTP(rec, req)

		require.Equal(t, http.StatusOK, rec.Code)
		var payload ProjectDTO
		require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
		require.Equal(t, project.Identity, payload.ID)
		require.Equal(t, originalName, payload.Name)
		require.Equal(t, originalIdentifier, payload.Identifier)
		require.Empty(t, payload.Icon)
		require.Empty(t, payload.Background)
		require.Empty(t, payload.Description)
		var updated models.SenlinAgentProject
		require.NoError(t, models.DBService.First(&updated, project.ID).Error)
		require.Equal(t, originalName, updated.Name)
		require.Equal(t, originalIdentifier, updated.Identifier)
		require.Empty(t, updated.Icon)
		require.Empty(t, updated.Background)
		require.Empty(t, updated.Description)
	})

	t.Run("returns not found when the identity is not owned by the current user", func(t *testing.T) {
		router, _, _ := newProjectsHandlerTestRouter(t)
		other, err := NewService().CreateProject(2, "Other", "")
		require.NoError(t, err)
		body, err := json.Marshal(gin.H{"name": "不可见项目"})
		require.NoError(t, err)
		req := httptest.NewRequest(http.MethodPatch, "/api/v1/projects/"+other.Identity, bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer test-token")
		rec := httptest.NewRecorder()

		router.ServeHTTP(rec, req)

		require.Equal(t, http.StatusNotFound, rec.Code)
		var payload httpx.ErrorEnvelope
		require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
		require.Equal(t, "not_found", payload.Error.Code)
		require.Equal(t, "项目不存在", payload.Error.Message)
	})

	t.Run("returns conflict for another owned project identifier", func(t *testing.T) {
		router, project, _ := newProjectsHandlerTestRouter(t)
		_, err := NewService().CreateProjectWithInput(1, CreateProjectRequest{Name: "Beta", Identifier: "BETA"})
		require.NoError(t, err)
		body, err := json.Marshal(gin.H{"identifier": "BETA"})
		require.NoError(t, err)
		req := httptest.NewRequest(http.MethodPatch, "/api/v1/projects/"+project.Identity, bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer test-token")
		rec := httptest.NewRecorder()

		router.ServeHTTP(rec, req)

		require.Equal(t, http.StatusConflict, rec.Code)
		var payload httpx.ErrorEnvelope
		require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
		require.Equal(t, "conflict", payload.Error.Code)
		require.Equal(t, "项目标识已存在", payload.Error.Message)
	})
}

func newProjectsHandlerTestRouter(t *testing.T) (*gin.Engine, *models.SenlinAgentProject, string) {
	t.Helper()
	newTestDB(t)
	require.NoError(t, models.DBService.Create(&models.SenlinAgentUser{Email: "david@example.com", DisplayName: "David", PasswordHash: "hash"}).Error)
	service := NewService()
	project, err := service.CreateProject(1, "Alpha", "")
	require.NoError(t, err)
	router := httpx.NewProtectedRouter(
		config.Config{Env: "test"},
		func(token string) (uint, error) { return 1, nil },
		NewHandler(service),
	)
	return router, project, ""
}

func mapKeys(value map[string]any) []string {
	keys := make([]string, 0, len(value))
	for key := range value {
		keys = append(keys, key)
	}
	return keys
}
