package memorywatcher

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestBuildCommitMessageChannelWake(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID:     "agt_alice",
		ChannelID:   "#engineering",
		ChannelType: "channel",
	})

	require.Equal(t, "Update memory from channel #engineering", firstLine(msg))
	require.Contains(t, msg, "Channel: #engineering")
	require.NotContains(t, msg, "Thread:")
	require.NotContains(t, msg, "Task:")
	require.Contains(t, msg, "AgentTeam-Channel: #engineering")
	require.NotContains(t, msg, "AgentTeam-Agent:")
	require.NotContains(t, msg, "AgentTeam-Thread:")
	require.NotContains(t, msg, "AgentTeam-Task:")
}

func TestBuildCommitMessageThreadWakeWithSubject(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID:      "agt_alice",
		ChannelID:    "#engineering",
		ChannelType:  "thread",
		ThreadTarget: "#engineering:abc123",
		ThreadTitle:  "Memory integration plan",
	})

	require.Equal(t, "Update memory from thread #engineering:abc123", firstLine(msg))
	body := extractBody(msg)
	require.Contains(t, body, "Channel: #engineering")
	require.Contains(t, body, "Thread: #engineering:abc123 — Memory integration plan")
	require.Contains(t, msg, "AgentTeam-Thread: #engineering:abc123")
}

func TestBuildCommitMessageThreadWakeWithoutSubject(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID:      "agt_alice",
		ChannelID:    "#engineering",
		ChannelType:  "thread",
		ThreadTarget: "#engineering:abc123",
	})

	require.Contains(t, msg, "Thread: #engineering:abc123\n")
	require.NotContains(t, msg, " — ")
}

func TestBuildCommitMessageTaskWake(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID:      "agt_alice",
		ChannelID:    "#agentteam",
		ThreadTarget: "#agentteam:abc123",
		TaskScope:    "#tsk_tk8msx2pmcjpw9ujlqn0",
		TaskNumber:   9,
		TaskTitle:    "Fix reply routing",
	})

	require.Equal(t, "Update memory from task #t9 in #agentteam:abc123", firstLine(msg))
	body := extractBody(msg)
	require.Contains(t, body, "Channel: #agentteam")
	require.Contains(t, body, "Thread: #agentteam:abc123")
	require.Contains(t, body, "Task: #t9 (#tsk_tk8msx2pmcjpw9ujlqn0) in #agentteam:abc123 — Fix reply routing")
	require.Contains(t, msg, "AgentTeam-Task: #t9")
	require.Contains(t, msg, "AgentTeam-Task-Scope: #tsk_tk8msx2pmcjpw9ujlqn0")
}

func TestBuildCommitMessageTaskWakeNumberOnly(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID:    "agt_alice",
		ChannelID:  "#agentteam",
		TaskNumber: 9,
	})

	require.Equal(t, "Update memory from task #t9 in #agentteam", firstLine(msg))
	require.Contains(t, msg, "Task: #t9 in #agentteam")
}

func TestBuildCommitMessageTaskWakeScopeOnly(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID:   "agt_alice",
		ChannelID: "#agentteam",
		TaskScope: "#tsk_tk8msx2pmcjpw9ujlqn0",
	})

	require.Equal(t, "Update memory from task #tsk_tk8msx2pmcjpw9ujlqn0 in #agentteam", firstLine(msg))
	require.Contains(t, msg, "Task: #tsk_tk8msx2pmcjpw9ujlqn0 in #agentteam")
}

func TestBuildCommitMessageTaskWakeIncludesTriggerOnlyInBody(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID:       "agt_alice",
		ChannelID:     "#agentteam",
		ThreadTarget:  "#agentteam:abc123",
		TaskNumber:    9,
		TriggerSource: "task_tool_call",
	})

	require.Equal(t, "Update memory from task #t9 in #agentteam:abc123", firstLine(msg))
	body := extractBody(msg)
	require.Contains(t, body, "Trigger: task_tool_call")
	require.NotContains(t, firstLine(msg), "task_tool_call")
	require.NotContains(t, msg, "AgentTeam-Task: task_tool_call")
}

func TestBuildCommitMessageDMWake(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID:     "agt_alice",
		ChannelType: "dm",
	})

	require.Equal(t, "Update memory from DM", firstLine(msg))
	require.Contains(t, msg, "Conversation: Direct Message")
	require.NotContains(t, msg, "Channel:")
}

func TestBuildCommitMessageDMTaskWakeCarriesDMWhere(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID:     "agt_alice",
		ChannelType: "dm",
		TaskScope:   "#tsk_xxx",
		TaskNumber:  3,
	})
	body := extractBody(msg)
	require.Contains(t, body, "Task: #t3 (#tsk_xxx) in DM")
}

func TestBuildCommitMessageNoContextFallsBackToLocalEdits(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{
		AgentID: "agt_alice",
	})

	require.Equal(t, "Update memory from local edits", firstLine(msg))
	// No body when there is no channel/thread/task context.
	body := extractBody(msg)
	require.Empty(t, body, "fallback commit must not have body lines")
	require.NotContains(t, msg, "AgentTeam-Agent:")
}

func TestBuildCommitMessageEmptyContextNoTrailers(t *testing.T) {
	t.Parallel()

	msg := BuildCommitMessage(CommitContext{})
	require.Equal(t, "Update memory from local edits\n", msg)
}

func TestTrailersOrderIsStable(t *testing.T) {
	t.Parallel()

	ctx := CommitContext{
		AgentID:      "agt_alice",
		ChannelID:    "#agentteam",
		ThreadTarget: "#agentteam:abc",
		TaskNumber:   3,
		TaskScope:    "#tsk_xxx",
	}

	got := ctx.Trailers()
	require.Equal(t,
		[]string{
			"AgentTeam-Channel: #agentteam",
			"AgentTeam-Thread: #agentteam:abc",
			"AgentTeam-Task: #t3",
			"AgentTeam-Task-Scope: #tsk_xxx",
		},
		got,
	)
}

func TestAmendKeyMatchesWhenSameScope(t *testing.T) {
	t.Parallel()

	ctx := CommitContext{
		ChannelID:    "#agentteam",
		ThreadTarget: "#agentteam:abc",
		TaskNumber:   3,
		TaskScope:    "#tsk_xxx",
	}
	trailers := map[string]string{
		"AgentTeam-Channel":    "#agentteam",
		"AgentTeam-Thread":     "#agentteam:abc",
		"AgentTeam-Task":       "#t3",
		"AgentTeam-Task-Scope": "#tsk_xxx",
	}
	require.Equal(t, ctx.AmendKey(), AmendKeyFromTrailers(trailers))
}

func TestAmendKeyDiffersWhenTaskDiffers(t *testing.T) {
	t.Parallel()

	now := CommitContext{ChannelID: "#agentteam", ThreadTarget: "#agentteam:abc", TaskScope: "#tsk_aaa"}
	prev := map[string]string{
		"AgentTeam-Channel": "#agentteam",
		"AgentTeam-Thread":  "#agentteam:abc",
		"AgentTeam-Task":    "#tsk_bbb",
	}
	require.NotEqual(t, now.AmendKey(), AmendKeyFromTrailers(prev))
}

func TestAmendKeyDiffersWhenChannelDiffers(t *testing.T) {
	t.Parallel()

	now := CommitContext{ChannelID: "#engineering"}
	prev := map[string]string{"AgentTeam-Channel": "#agentteam"}
	require.NotEqual(t, now.AmendKey(), AmendKeyFromTrailers(prev))
}

func TestAmendKeyFromEmptyTrailersIsEmpty(t *testing.T) {
	t.Parallel()

	require.Empty(t, AmendKeyFromTrailers(nil))
	require.Empty(t, AmendKeyFromTrailers(map[string]string{}))
}

// firstLine returns the subject of a commit message (everything before the
// first blank line).
func firstLine(msg string) string {
	if i := strings.Index(msg, "\n"); i >= 0 {
		return msg[:i]
	}
	return msg
}

// extractBody returns the body section between subject and trailers. Used
// to assert body-only content without leaking trailers into expectations.
// Returns empty string when the message has no body (subject only, or
// subject + trailers with nothing between).
func extractBody(msg string) string {
	parts := strings.SplitN(msg, "\n\n", 3)
	if len(parts) < 3 {
		return ""
	}
	return parts[1]
}
