package projects

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

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

func TestTagRegistrarUsesIdentityAndListsOnlyProjectTags(t *testing.T) {
	database := newTestDB(t)
	require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
	first := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
	second := models.SenlinAgentProject{OwnerID: 1, Name: "Beta", Identifier: "BETA"}
	require.NoError(t, database.Create(&first).Error)
	require.NoError(t, database.Create(&second).Error)
	require.NoError(t, database.Create(&models.SenlinAgentTag{ProjectID: second.ID, Name: "其他项目"}).Error)
	router := newProjectWriteHandlerRouter(t, 1, NewTagHandler(NewService()))
	createReq := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+first.Identity+"/tags", bytes.NewBufferString(`{"name":"设计"}`))
	createReq.Header.Set("Content-Type", "application/json")
	createReq.Header.Set("Authorization", "Bearer test-token")
	createRec := httptest.NewRecorder()

	router.ServeHTTP(createRec, createReq)

	require.Equal(t, http.StatusCreated, createRec.Code, createRec.Body.String())
	var created map[string]any
	require.NoError(t, json.Unmarshal(createRec.Body.Bytes(), &created))
	require.NotEmpty(t, created["id"])
	require.Equal(t, "设计", created["name"])
	require.ElementsMatch(t, []string{"id", "name"}, mapKeys(created))

	listReq := httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+first.Identity+"/tags", nil)
	listReq.Header.Set("Authorization", "Bearer test-token")
	listRec := httptest.NewRecorder()
	router.ServeHTTP(listRec, listReq)
	require.Equal(t, http.StatusOK, listRec.Code, listRec.Body.String())
	var listed []WorkspaceTagDTO
	require.NoError(t, json.Unmarshal(listRec.Body.Bytes(), &listed))
	require.Len(t, listed, 1)
	require.Equal(t, "设计", listed[0].Name)
}

func TestTagRegistrarRejectsProjectOwnedByAnotherUser(t *testing.T) {
	database := newTestDB(t)
	require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
	other := models.SenlinAgentProject{OwnerID: 2, Name: "Private", Identifier: "PRIVATE"}
	require.NoError(t, database.Create(&other).Error)
	router := newProjectWriteHandlerRouter(t, 1, NewTagHandler(NewService()))
	req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+other.Identity+"/tags", bytes.NewBufferString(`{"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.StatusNotFound, rec.Code, rec.Body.String())
	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)
}

func TestTagAndCronServicesReturnTypedValidationErrors(t *testing.T) {
	database := newTestDB(t)
	require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
	project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
	require.NoError(t, database.Create(&project).Error)
	service := NewService()

	_, err := service.CreateProjectTag(1, project.ID, " ")
	require.ErrorIs(t, err, ErrTagNameRequired)
	_, err = service.CreateCronPlan(1, project.ID, CreateCronPlanInput{Title: " ", Schedule: "0 9 * * *"})
	require.ErrorIs(t, err, ErrCronTitleRequired)
	_, err = service.CreateCronPlan(1, project.ID, CreateCronPlanInput{Title: "Daily", Schedule: " "})
	require.ErrorIs(t, err, ErrCronScheduleRequired)
}

func TestTagRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
	database := newTestDB(t)
	require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
	project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
	require.NoError(t, database.Create(&project).Error)
	require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:fail_tag_create", func(tx *gorm.DB) {
		if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SenlinAgentTag{}).TableName() {
			tx.AddError(errors.New("forced tag create failure"))
		}
	}))
	router := newProjectWriteHandlerRouter(t, 1, NewTagHandler(NewService()))
	req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+project.Identity+"/tags", bytes.NewBufferString(`{"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.StatusInternalServerError, rec.Code, rec.Body.String())
	var payload httpx.ErrorEnvelope
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Equal(t, "internal_error", payload.Error.Code)
	require.Equal(t, "标签创建失败", payload.Error.Message)
}

func TestCronRegistrarMapsDatabaseFailureToInternalError(t *testing.T) {
	database := newTestDB(t)
	require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
	project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
	require.NoError(t, database.Create(&project).Error)
	require.NoError(t, database.Callback().Create().Before("gorm:create").Register("test:fail_cron_create", func(tx *gorm.DB) {
		if tx.Statement.Schema != nil && tx.Statement.Schema.Table == (models.SenlinAgentCronPlan{}).TableName() {
			tx.AddError(errors.New("forced cron create failure"))
		}
	}))
	router := newProjectWriteHandlerRouter(t, 1, NewCronHandler(NewService()))
	req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+project.Identity+"/cron-plans", bytes.NewBufferString(`{"title":"每日整理","schedule":"0 9 * * *"}`))
	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.StatusInternalServerError, rec.Code, rec.Body.String())
	var payload httpx.ErrorEnvelope
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Equal(t, "internal_error", payload.Error.Code)
	require.Equal(t, "计划任务创建失败", payload.Error.Message)
}

func TestCronRegistrarCreatesCamelCaseIdentityDTO(t *testing.T) {
	database := newTestDB(t)
	require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
	project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
	require.NoError(t, database.Create(&project).Error)
	router := newProjectWriteHandlerRouter(t, 1, NewCronHandler(NewService()))
	req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+project.Identity+"/cron-plans", bytes.NewBufferString(`{"title":"每日整理","schedule":"0 9 * * *","enabled":true,"nextRunAt":"2026-07-22T08:00:00Z"}`))
	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, rec.Body.String())
	var plan models.SenlinAgentCronPlan
	require.NoError(t, database.Where("project_id = ?", project.ID).First(&plan).Error)
	var payload map[string]any
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
	require.Equal(t, plan.Identity, payload["id"])
	require.Equal(t, project.Identity, payload["projectId"])
	require.Equal(t, "2026-07-22T08:00:00Z", payload["nextRunAt"])
	require.NotContains(t, payload, "ProjectID")
}

func newProjectWriteHandlerRouter(t *testing.T, currentUserID uint, registrar httpx.RouteRegistrar) *gin.Engine {
	t.Helper()
	return httpx.NewProtectedRouter(
		config.Config{Env: "test"},
		func(string) (uint, error) { return currentUserID, nil },
		registrar,
	)
}
