package tasktimeline

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

func TestNewTypedActivityIntentDefaultsToUnwatchedActorParticipant(t *testing.T) {
	createdAt := time.Date(2026, 5, 25, 12, 0, 0, 0, time.UTC)
	activity := NewTypedActivityIntent(" task_1 ", " mem_1 ", " status_changed ", " done ", createdAt)
	require.Equal(t, "task_1", activity.TaskID)
	require.Equal(t, "mem_1", activity.ActorRef)
	require.Equal(t, "status_changed", activity.EventType)
	require.Equal(t, "done", activity.EventPayload)
	require.Equal(t, ActorParticipantModeDefaultUnwatched, activity.ActorParticipantMode)
	require.Equal(t, createdAt, activity.CreatedAt)
}

func TestTypedActivityProjectionsFromIntentsPreservesExplicitNoParticipantMode(t *testing.T) {
	activity := NewTypedActivityIntent("task_1", "mem_1", "discussion_added", "msg_1", time.Now().UTC())
	activity.ActorParticipantMode = ActorParticipantModeNone

	normalized, err := typedActivityProjectionsFromIntents([]TypedActivityIntent{activity})

	require.NoError(t, err)
	require.Len(t, normalized, 1)
	require.Equal(t, ActorParticipantModeNone, normalized[0].ActorParticipantMode)
}

func TestTypedActivityProjectionsFromIntentsRejectsUnknownParticipantMode(t *testing.T) {
	activity := NewTypedActivityIntent("task_1", "mem_1", "status_changed", "done", time.Now().UTC())
	activity.ActorParticipantMode = ActorParticipantMode("mystery")

	_, err := typedActivityProjectionsFromIntents([]TypedActivityIntent{activity})

	require.ErrorContains(t, err, "unknown task timeline actor participant mode")
}

func TestMarshalTypedActivityPayloadUsesTaskEventEnvelope(t *testing.T) {
	payload := marshalTypedActivityPayload(TypedActivityProjection{
		EventID:      "tev_1",
		TaskID:       "tsk_1",
		ActorRef:     "mem_1",
		EventType:    "status_changed",
		EventPayload: "done",
	})

	var decoded map[string]any
	require.NoError(t, json.Unmarshal(payload, &decoded))
	require.Equal(t, map[string]any{"id": "tsk_1", "scopeId": "tsk_1"}, decoded["task"])
	require.Equal(t, map[string]any{
		"eventId":  "tev_1",
		"taskId":   "tsk_1",
		"type":     "status_changed",
		"payload":  "done",
		"actorRef": "mem_1",
	}, decoded["taskEvent"])
}

func TestMarshalTypedActivityPayloadOmitsEmptyActorRef(t *testing.T) {
	payload := marshalTypedActivityPayload(TypedActivityProjection{
		EventID:      "tev_1",
		TaskID:       "tsk_1",
		EventType:    "status_changed",
		EventPayload: "done",
	})

	var decoded map[string]map[string]any
	require.NoError(t, json.Unmarshal(payload, &decoded))
	_, ok := decoded["taskEvent"]["actorRef"]
	require.False(t, ok)
}

func TestEnsureTaskTimelineParticipantNotificationDefaultInsertsWithoutNotificationReadBeforeWrite(t *testing.T) {
	now := time.Date(2026, 6, 13, 8, 0, 0, 0, time.UTC)
	execer := &recordingTaskTimelineQueryExecer{
		execRowsAffected: []int64{1, 1},
		rowResults: []recordingTaskTimelineRowResult{
			{scopeType: "task"},
		},
	}

	err := ensureTaskScopeParticipantNotification(context.Background(), execer, " task_scope_1 ", " mem_1 ", "mute", now)

	require.NoError(t, err)
	require.Len(t, execer.queries, 3)
	require.Contains(t, execer.queries[0], "SELECT scope.type")
	require.Contains(t, execer.queries[0], "LEFT JOIN scopes parent")
	require.Contains(t, execer.queries[1], "UPDATE scope_memberships")
	require.Contains(t, execer.queries[2], "INSERT INTO scope_notification_settings")
	require.Contains(t, execer.queries[2], "task_watch_state")
	for _, query := range execer.queries {
		require.NotContains(t, query, "FROM scope_notification_settings")
	}
}

type recordingTaskTimelineQueryExecer struct {
	queries          []string
	execRowsAffected []int64
	rowResults       []recordingTaskTimelineRowResult
}

func (execer *recordingTaskTimelineQueryExecer) Exec(_ context.Context, query string, _ ...any) (coredb.Result, error) {
	execer.queries = append(execer.queries, query)
	if len(execer.execRowsAffected) == 0 {
		return recordingTaskTimelineResult{}, nil
	}
	rowsAffected := execer.execRowsAffected[0]
	execer.execRowsAffected = execer.execRowsAffected[1:]
	return recordingTaskTimelineResult{rowsAffected: rowsAffected}, nil
}

func (execer *recordingTaskTimelineQueryExecer) QueryRow(_ context.Context, query string, _ ...any) coredb.Row {
	execer.queries = append(execer.queries, query)
	if len(execer.rowResults) == 0 {
		return recordingTaskTimelineRowResult{err: errors.New("unexpected QueryRow call")}
	}
	result := execer.rowResults[0]
	execer.rowResults = execer.rowResults[1:]
	return result
}

func (execer *recordingTaskTimelineQueryExecer) Query(context.Context, string, ...any) (coredb.Rows, error) {
	return nil, errors.New("unexpected Query call")
}

type recordingTaskTimelineResult struct {
	rowsAffected int64
}

func (result recordingTaskTimelineResult) RowsAffected() int64 {
	return result.rowsAffected
}

type recordingTaskTimelineRowResult struct {
	scopeType  string
	parentID   sql.NullString
	parentType string
	err        error
}

func (row recordingTaskTimelineRowResult) Scan(dest ...any) error {
	if row.err != nil {
		return row.err
	}
	if len(dest) != 3 {
		return errors.New("unexpected QueryRow scan shape")
	}
	scopeType, scopeTypeOK := dest[0].(*string)
	parentID, parentIDOK := dest[1].(*sql.NullString)
	parentType, parentTypeOK := dest[2].(*string)
	if !scopeTypeOK || !parentIDOK || !parentTypeOK {
		return errors.New("unexpected QueryRow destination types")
	}
	*scopeType = row.scopeType
	*parentID = row.parentID
	*parentType = row.parentType
	return nil
}

type recordingTaskTimelineUnexpectedRow struct{}

func (recordingTaskTimelineUnexpectedRow) Scan(...any) error {
	return errors.New("unexpected QueryRow call")
}
