package chat

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

// ===== conversation fake repo extension =====
// convFakeRepo embeds fakeRepo and overrides conversation/template/model methods.
type convFakeRepo struct {
	*fakeRepo
	mu            sync.Mutex
	conversations map[uuid.UUID]*Conversation
	messages      map[uuid.UUID][]Message
	templates     map[uuid.UUID]*PromptTemplate
	titleUpdated  map[uuid.UUID]*string
	softDeleted   []uuid.UUID
}

func newConvFakeRepo() *convFakeRepo {
	return &convFakeRepo{
		fakeRepo:      newFakeRepo(),
		conversations: make(map[uuid.UUID]*Conversation),
		messages:      make(map[uuid.UUID][]Message),
		templates:     make(map[uuid.UUID]*PromptTemplate),
		titleUpdated:  make(map[uuid.UUID]*string),
	}
}

func (r *convFakeRepo) InsertConversation(_ context.Context, p InsertConversationParams) (*Conversation, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	c := &Conversation{
		ID:            p.ID,
		UserID:        p.UserID,
		ProjectID:     p.ProjectID,
		WorkspaceID:   p.WorkspaceID,
		IssueID:       p.IssueID,
		RequirementID: p.RequirementID,
		ModelID:       p.ModelID,
		SystemPrompt:  p.SystemPrompt,
		Title:         p.Title,
		CreatedAt:     time.Now(),
		UpdatedAt:     time.Now(),
	}
	r.conversations[p.ID] = c
	return c, nil
}

func (r *convFakeRepo) GetConversation(_ context.Context, id uuid.UUID) (*Conversation, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	c, ok := r.conversations[id]
	if !ok {
		return nil, errs.New(errs.CodeChatConversationNotFound, "conversation not found")
	}
	return c, nil
}

func (r *convFakeRepo) ListConversationsByUser(_ context.Context, userID uuid.UUID, f ConversationFilter) ([]Conversation, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	var out []Conversation
	for _, c := range r.conversations {
		if c.UserID != userID || c.DeletedAt != nil {
			continue
		}
		if f.RequirementID != nil && (c.RequirementID == nil || *c.RequirementID != *f.RequirementID) {
			continue
		}
		out = append(out, *c)
	}
	limit := f.Limit
	if limit > 0 && len(out) > limit {
		out = out[:limit]
	}
	return out, nil
}

func (r *convFakeRepo) SetConversationTitleManual(_ context.Context, id uuid.UUID, title *string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	c, ok := r.conversations[id]
	if !ok {
		return errs.New(errs.CodeChatConversationNotFound, "conversation not found")
	}
	c.Title = title
	r.titleUpdated[id] = title
	return nil
}

func (r *convFakeRepo) UpdateConversationTitle(_ context.Context, id uuid.UUID, title *string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	c, ok := r.conversations[id]
	if !ok {
		return errs.New(errs.CodeChatConversationNotFound, "conversation not found")
	}
	c.Title = title
	r.titleUpdated[id] = title
	return nil
}

func (r *convFakeRepo) SoftDeleteConversation(_ context.Context, id uuid.UUID) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	c, ok := r.conversations[id]
	if !ok {
		return errs.New(errs.CodeChatConversationNotFound, "conversation not found")
	}
	now := time.Now()
	c.DeletedAt = &now
	r.softDeleted = append(r.softDeleted, id)
	return nil
}

func (r *convFakeRepo) ListAttachmentsForMessage(_ context.Context, _ int64) ([]Attachment, error) {
	return nil, nil
}

func (r *convFakeRepo) ListMessagesByConversation(_ context.Context, convID uuid.UUID, _ *int64, limit int) ([]Message, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	msgs := r.messages[convID]
	if limit > 0 && len(msgs) > limit {
		msgs = msgs[:limit]
	}
	return msgs, nil
}

func (r *convFakeRepo) GetLLMModel(_ context.Context, id uuid.UUID) (*LLMModel, error) {
	for _, m := range r.fakeRepo.models {
		if m.ID == id {
			cp := m
			return &cp, nil
		}
	}
	return nil, errs.New(errs.CodeChatModelNotFound, "model not found")
}

func (r *convFakeRepo) GetPromptTemplate(_ context.Context, id uuid.UUID) (*PromptTemplate, error) {
	t, ok := r.templates[id]
	if !ok {
		return nil, errs.New(errs.CodeChatTemplateNotFound, "template not found")
	}
	return t, nil
}

// ===== recording auditor =====

type recordingAuditor struct {
	mu      sync.Mutex
	entries []audit.Entry
}

func (a *recordingAuditor) Record(_ context.Context, e audit.Entry) error {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.entries = append(a.entries, e)
	return nil
}

func (a *recordingAuditor) lastEntry() (audit.Entry, bool) {
	a.mu.Lock()
	defer a.mu.Unlock()
	if len(a.entries) == 0 {
		return audit.Entry{}, false
	}
	return a.entries[len(a.entries)-1], true
}

// ===== helpers =====

func newConvSvc(repo *convFakeRepo, aud audit.Recorder) ConversationService {
	return NewConversationService(repo, nil, aud, discardLogger())
}

func addModel(repo *convFakeRepo, enabled bool) LLMModel {
	m := LLMModel{
		ID:      uuid.Must(uuid.NewV7()),
		ModelID: "test-model",
		Enabled: enabled,
	}
	repo.fakeRepo.models = append(repo.fakeRepo.models, m)
	return m
}

func addTemplate(repo *convFakeRepo, scope TemplateScope, ownerID *uuid.UUID) *PromptTemplate {
	t := &PromptTemplate{
		ID:      uuid.Must(uuid.NewV7()),
		Name:    "test-tpl",
		Content: "system prompt from template",
		Scope:   scope,
		OwnerID: ownerID,
	}
	repo.templates[t.ID] = t
	return t
}

func addConversation(repo *convFakeRepo, userID uuid.UUID) *Conversation {
	c := &Conversation{
		ID:        uuid.Must(uuid.NewV7()),
		UserID:    userID,
		CreatedAt: time.Now(),
		UpdatedAt: time.Now(),
	}
	repo.conversations[c.ID] = c
	return c
}

// ===== tests =====

func TestConversationService_Create_HappyPath(t *testing.T) {
	t.Helper()
	repo := newConvFakeRepo()
	aud := &recordingAuditor{}
	svc := newConvSvc(repo, aud)
	userID := uuid.Must(uuid.NewV7())
	m := addModel(repo, true)

	c, err := svc.Create(context.Background(), userID, CreateConversationInput{ModelID: m.ID})
	require.NoError(t, err)
	require.NotNil(t, c)
	assert.Equal(t, userID, c.UserID)
	assert.Equal(t, m.ID, c.ModelID)
	assert.Empty(t, c.SystemPrompt)

	entry, ok := aud.lastEntry()
	require.True(t, ok)
	assert.Equal(t, "chat.conversation.created", entry.Action)
	assert.Equal(t, &userID, entry.UserID)
}

func TestConversationService_Create_WithRequirementMount(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())
	m := addModel(repo, true)

	reqID := uuid.Must(uuid.NewV7())
	projID := uuid.Must(uuid.NewV7())
	c, err := svc.Create(context.Background(), userID, CreateConversationInput{
		ModelID: m.ID, ProjectID: &projID, RequirementID: &reqID,
	})
	require.NoError(t, err)
	require.NotNil(t, c.RequirementID)
	assert.Equal(t, reqID, *c.RequirementID)

	// List 按 requirement 过滤：命中 1 条；其他 requirement 不命中。
	_, err = svc.Create(context.Background(), userID, CreateConversationInput{ModelID: m.ID})
	require.NoError(t, err)
	list, err := svc.List(context.Background(), userID, ConversationFilter{RequirementID: &reqID})
	require.NoError(t, err)
	require.Len(t, list, 1)
	other := uuid.Must(uuid.NewV7())
	list, err = svc.List(context.Background(), userID, ConversationFilter{RequirementID: &other})
	require.NoError(t, err)
	require.Empty(t, list)
}

func TestConversationService_Create_WithSystemTemplate(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())
	m := addModel(repo, true)
	tpl := addTemplate(repo, TemplateScopeSystem, nil)

	c, err := svc.Create(context.Background(), userID, CreateConversationInput{
		ModelID:    m.ID,
		TemplateID: &tpl.ID,
	})
	require.NoError(t, err)
	assert.Equal(t, "system prompt from template", c.SystemPrompt)
}

func TestConversationService_Create_TemplateUserScopeOwnerMismatch(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())
	otherID := uuid.Must(uuid.NewV7())
	m := addModel(repo, true)
	tpl := addTemplate(repo, TemplateScopeUser, &otherID)

	_, err := svc.Create(context.Background(), userID, CreateConversationInput{
		ModelID:    m.ID,
		TemplateID: &tpl.ID,
	})
	require.Error(t, err)
	var appErr *errs.AppError
	require.ErrorAs(t, err, &appErr)
	assert.Equal(t, errs.CodeForbidden, appErr.Code)
}

func TestConversationService_Create_WithSystemPromptOverride(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())
	m := addModel(repo, true)
	tpl := addTemplate(repo, TemplateScopeSystem, nil)
	override := "custom system prompt"

	c, err := svc.Create(context.Background(), userID, CreateConversationInput{
		ModelID:      m.ID,
		TemplateID:   &tpl.ID,
		SystemPrompt: &override,
	})
	require.NoError(t, err)
	// override takes precedence; template content ignored
	assert.Equal(t, override, c.SystemPrompt)
}

func TestConversationService_Create_ModelNotFound(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())

	_, err := svc.Create(context.Background(), userID, CreateConversationInput{
		ModelID: uuid.Must(uuid.NewV7()),
	})
	require.Error(t, err)
	var appErr *errs.AppError
	require.ErrorAs(t, err, &appErr)
	assert.Equal(t, errs.CodeChatModelNotFound, appErr.Code)
}

func TestConversationService_Create_ModelDisabled(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())
	m := addModel(repo, false)

	_, err := svc.Create(context.Background(), userID, CreateConversationInput{ModelID: m.ID})
	require.Error(t, err)
	var appErr *errs.AppError
	require.ErrorAs(t, err, &appErr)
	assert.Equal(t, errs.CodeChatModelNotFound, appErr.Code)
}

func TestConversationService_Create_TemplateNotFound(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())
	m := addModel(repo, true)
	missingID := uuid.Must(uuid.NewV7())

	_, err := svc.Create(context.Background(), userID, CreateConversationInput{
		ModelID:    m.ID,
		TemplateID: &missingID,
	})
	require.Error(t, err)
	var appErr *errs.AppError
	require.ErrorAs(t, err, &appErr)
	assert.Equal(t, errs.CodeChatTemplateNotFound, appErr.Code)
}

func TestConversationService_Get_HappyPath(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())
	conv := addConversation(repo, userID)
	repo.messages[conv.ID] = []Message{
		{ID: 1, ConversationID: conv.ID, Role: RoleUser, Content: "hello"},
	}

	got, msgs, err := svc.Get(context.Background(), userID, conv.ID, 50)
	require.NoError(t, err)
	require.NotNil(t, got)
	assert.Equal(t, conv.ID, got.ID)
	require.Len(t, msgs, 1)
	assert.Equal(t, "hello", msgs[0].Content)
}

func TestConversationService_Get_NotOwner(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	ownerID := uuid.Must(uuid.NewV7())
	callerID := uuid.Must(uuid.NewV7())
	conv := addConversation(repo, ownerID)

	_, _, err := svc.Get(context.Background(), callerID, conv.ID, 50)
	require.Error(t, err)
	var appErr *errs.AppError
	require.ErrorAs(t, err, &appErr)
	assert.Equal(t, errs.CodeForbidden, appErr.Code)
}

func TestConversationService_List_DefaultLimit(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())
	// Add 3 conversations
	for i := 0; i < 3; i++ {
		addConversation(repo, userID)
	}

	// Pass Limit=0 — should default to 50 internally (all 3 returned)
	convs, err := svc.List(context.Background(), userID, ConversationFilter{Limit: 0})
	require.NoError(t, err)
	assert.Len(t, convs, 3)
}

func TestConversationService_UpdateTitle_HappyPath(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	userID := uuid.Must(uuid.NewV7())
	conv := addConversation(repo, userID)

	err := svc.UpdateTitle(context.Background(), userID, conv.ID, "new title")
	require.NoError(t, err)
	// Verify title was written
	title, ok := repo.titleUpdated[conv.ID]
	require.True(t, ok)
	require.NotNil(t, title)
	assert.Equal(t, "new title", *title)
}

func TestConversationService_UpdateTitle_NotOwner(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	ownerID := uuid.Must(uuid.NewV7())
	callerID := uuid.Must(uuid.NewV7())
	conv := addConversation(repo, ownerID)

	err := svc.UpdateTitle(context.Background(), callerID, conv.ID, "hack")
	require.Error(t, err)
	var appErr *errs.AppError
	require.ErrorAs(t, err, &appErr)
	assert.Equal(t, errs.CodeForbidden, appErr.Code)
}

func TestConversationService_SoftDelete_HappyPath(t *testing.T) {
	repo := newConvFakeRepo()
	aud := &recordingAuditor{}
	svc := newConvSvc(repo, aud)
	userID := uuid.Must(uuid.NewV7())
	conv := addConversation(repo, userID)

	err := svc.SoftDelete(context.Background(), userID, conv.ID)
	require.NoError(t, err)
	require.Contains(t, repo.softDeleted, conv.ID)

	entry, ok := aud.lastEntry()
	require.True(t, ok)
	assert.Equal(t, "chat.conversation.deleted", entry.Action)
	assert.Equal(t, &userID, entry.UserID)
}

func TestConversationService_SoftDelete_NotOwner(t *testing.T) {
	repo := newConvFakeRepo()
	svc := newConvSvc(repo, noopAuditor{})
	ownerID := uuid.Must(uuid.NewV7())
	callerID := uuid.Must(uuid.NewV7())
	conv := addConversation(repo, ownerID)

	err := svc.SoftDelete(context.Background(), callerID, conv.ID)
	require.Error(t, err)
	var appErr *errs.AppError
	require.ErrorAs(t, err, &appErr)
	assert.Equal(t, errs.CodeForbidden, appErr.Code)
}
