package message

import (
	"context"
	"testing"

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

func validWorkflowSource() ContributionSource {
	return ContributionSource{
		Kind:      ContributionSourceKindWorkflowRun,
		RefType:   ContributionSourceRefTypeWorkflowRun,
		RefID:     "run_1",
		RunID:     "run_1",
		EventID:   "wrf_1",
		EventType: "manual",
		Label:     "Calculator",
	}
}

func validScheduleSource() ContributionSource {
	return ContributionSource{
		Kind:    ContributionSourceKindScheduleRun,
		RefType: ContributionSourceRefTypeAutomationRun,
		RefID:   "run_2",
		RunID:   "run_2",
		Label:   "Daily standup",
	}
}

func TestAutomationServiceAuthorForSource(t *testing.T) {
	tests := []struct {
		name      string
		source    ContributionSource
		wantKind  string
		wantName  string
		wantFound bool
	}{
		{
			name:      "valid workflow run",
			source:    validWorkflowSource(),
			wantKind:  AuthorServiceKindWorkflow,
			wantName:  "Calculator",
			wantFound: true,
		},
		{
			name:      "valid schedule run without event id",
			source:    validScheduleSource(),
			wantKind:  AuthorServiceKindSchedule,
			wantName:  "Daily standup",
			wantFound: true,
		},
		{
			name: "workflow missing event id is rejected",
			source: ContributionSource{
				Kind:    ContributionSourceKindWorkflowRun,
				RefType: ContributionSourceRefTypeWorkflowRun,
				RefID:   "run_1",
				RunID:   "run_1",
				Label:   "Calculator",
			},
			wantFound: false,
		},
		{
			name: "workflow missing ref id is rejected",
			source: ContributionSource{
				Kind:      ContributionSourceKindWorkflowRun,
				RefType:   ContributionSourceRefTypeWorkflowRun,
				RunID:     "run_1",
				EventID:   "wrf_1",
				EventType: "manual",
				Label:     "Calculator",
			},
			wantFound: false,
		},
		{
			name: "workflow run-id only is rejected",
			source: ContributionSource{
				Kind:      ContributionSourceKindWorkflowRun,
				RefType:   ContributionSourceRefTypeWorkflowRun,
				RunID:     "run_1",
				EventID:   "wrf_1",
				EventType: "manual",
				Label:     "Calculator",
			},
			wantFound: false,
		},
		{
			name: "workflow ref-id run-id mismatch is rejected",
			source: ContributionSource{
				Kind:      ContributionSourceKindWorkflowRun,
				RefType:   ContributionSourceRefTypeWorkflowRun,
				RefID:     "run_1",
				RunID:     "run_other",
				EventID:   "wrf_1",
				EventType: "manual",
				Label:     "Calculator",
			},
			wantFound: false,
		},
		{
			name: "workflow wrong ref type is rejected",
			source: ContributionSource{
				Kind:      ContributionSourceKindWorkflowRun,
				RefType:   ContributionSourceRefTypeMessage,
				RefID:     "run_1",
				RunID:     "run_1",
				EventID:   "wrf_1",
				EventType: "manual",
			},
			wantFound: false,
		},
		{
			name: "schedule missing ref id is rejected",
			source: ContributionSource{
				Kind:    ContributionSourceKindScheduleRun,
				RefType: ContributionSourceRefTypeAutomationRun,
				RunID:   "run_2",
				Label:   "Daily standup",
			},
			wantFound: false,
		},
		{
			name: "schedule ref-id run-id mismatch is rejected",
			source: ContributionSource{
				Kind:    ContributionSourceKindScheduleRun,
				RefType: ContributionSourceRefTypeAutomationRun,
				RefID:   "run_2",
				RunID:   "run_other",
				Label:   "Daily standup",
			},
			wantFound: false,
		},
		{
			name: "schedule wrong ref type is rejected",
			source: ContributionSource{
				Kind:    ContributionSourceKindScheduleRun,
				RefType: ContributionSourceRefTypeMessage,
				RefID:   "run_2",
				RunID:   "run_2",
				Label:   "Daily standup",
			},
			wantFound: false,
		},
		{
			name: "human compose is not automation",
			source: ContributionSource{
				Kind:    ContributionSourceKindHumanCompose,
				RefType: ContributionSourceRefTypeMessage,
				RefID:   "msg_1",
			},
			wantFound: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			profile, ok := AutomationServiceAuthorForSource(tt.source)
			if ok != tt.wantFound {
				t.Fatalf("AutomationServiceAuthorForSource() found = %v, want %v", ok, tt.wantFound)
			}
			if !ok {
				return
			}
			if profile.ActorType != AuthorActorTypeService {
				t.Errorf("ActorType = %q, want service", profile.ActorType)
			}
			if profile.ServiceKind != tt.wantKind {
				t.Errorf("ServiceKind = %q, want %q", profile.ServiceKind, tt.wantKind)
			}
			if profile.DisplayName != tt.wantName {
				t.Errorf("DisplayName = %q, want %q", profile.DisplayName, tt.wantName)
			}
			if profile.MemberID != "" {
				t.Errorf("MemberID must be empty for automation author, got %q", profile.MemberID)
			}
			if profile.PrincipalID == "" {
				t.Errorf("PrincipalID must carry the stable RefID for automation author")
			}
			if !profile.Valid() {
				t.Errorf("profile must be valid, got %+v", profile)
			}
		})
	}
}

func TestAutomationServiceAuthorDerivesStableIdentifiers(t *testing.T) {
	workflow, ok := AutomationServiceAuthorForSource(validWorkflowSource())
	require.True(t, ok)
	require.Equal(t, "run_1", workflow.PrincipalID)
	require.Equal(t, "wrf_1", workflow.TargetID)

	schedule, ok := AutomationServiceAuthorForSource(validScheduleSource())
	require.True(t, ok)
	require.Equal(t, "run_2", schedule.PrincipalID)
	require.Equal(t, "run_2", schedule.TargetID, "schedule without event id falls back to ref id")

	scheduleWithEvent := validScheduleSource()
	scheduleWithEvent.EventID = "occ_9"
	scheduleWithEvent.EventType = "time"
	schedule, ok = AutomationServiceAuthorForSource(scheduleWithEvent)
	require.True(t, ok)
	require.Equal(t, "run_2", schedule.PrincipalID)
	require.Equal(t, "occ_9", schedule.TargetID)
}

func TestAutomationSourceIdentifiesRun(t *testing.T) {
	// A source satisfies the durable native write contract or it does not
	// identify an automation run at all. ID-less and internally inconsistent
	// sources must be rejected.
	for _, tt := range []struct {
		name   string
		source ContributionSource
		want   bool
	}{
		{name: "valid workflow", source: validWorkflowSource(), want: true},
		{name: "valid schedule", source: validScheduleSource(), want: true},
		{name: "workflow missing event id", source: ContributionSource{Kind: ContributionSourceKindWorkflowRun, RefType: ContributionSourceRefTypeWorkflowRun, RefID: "run_1", RunID: "run_1"}, want: false},
		{name: "workflow missing ref id", source: ContributionSource{Kind: ContributionSourceKindWorkflowRun, RefType: ContributionSourceRefTypeWorkflowRun, RunID: "run_1", EventID: "wrf_1"}, want: false},
		{name: "workflow run id only", source: ContributionSource{Kind: ContributionSourceKindWorkflowRun, RefType: ContributionSourceRefTypeWorkflowRun, RunID: "run_1", EventID: "wrf_1"}, want: false},
		{name: "workflow ref run mismatch", source: ContributionSource{Kind: ContributionSourceKindWorkflowRun, RefType: ContributionSourceRefTypeWorkflowRun, RefID: "run_1", RunID: "run_other", EventID: "wrf_1"}, want: false},
		{name: "workflow wrong ref type", source: ContributionSource{Kind: ContributionSourceKindWorkflowRun, RefType: ContributionSourceRefTypeMessage, RefID: "run_1", RunID: "run_1", EventID: "wrf_1"}, want: false},
		{name: "schedule missing ref id", source: ContributionSource{Kind: ContributionSourceKindScheduleRun, RefType: ContributionSourceRefTypeAutomationRun, RunID: "run_2"}, want: false},
		{name: "schedule ref run mismatch", source: ContributionSource{Kind: ContributionSourceKindScheduleRun, RefType: ContributionSourceRefTypeAutomationRun, RefID: "run_2", RunID: "run_other"}, want: false},
		{name: "schedule wrong ref type", source: ContributionSource{Kind: ContributionSourceKindScheduleRun, RefType: ContributionSourceRefTypeMessage, RefID: "run_2", RunID: "run_2"}, want: false},
		{name: "human compose", source: ContributionSource{Kind: ContributionSourceKindHumanCompose, RefType: ContributionSourceRefTypeMessage, RefID: "msg_1"}, want: false},
	} {
		t.Run(tt.name, func(t *testing.T) {
			if got := AutomationSourceIdentifiesRun(tt.source); got != tt.want {
				t.Fatalf("AutomationSourceIdentifiesRun() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestApplyAutomationServiceAuthorToAppMessage(t *testing.T) {
	t.Run("human-owned workflow row becomes Workflow author", func(t *testing.T) {
		message := AppMessage{
			Source: validWorkflowSource(),
			AuthorProfile: AuthorProfile{
				ActorType:   AuthorActorTypeHuman,
				PrincipalID: "usr_owner",
				MemberID:    "mem_owner",
				DisplayName: "Owner",
			},
		}
		ApplyAutomationServiceAuthorToAppMessage(&message)
		if message.AuthorActorType() != AuthorActorTypeService {
			t.Fatalf("AuthorActorType = %q, want service", message.AuthorActorType())
		}
		if message.AuthorDisplayName() != "Calculator" {
			t.Errorf("AuthorDisplayName = %q, want Calculator", message.AuthorDisplayName())
		}
		if message.AuthorMemberID() != "" {
			t.Errorf("AuthorMemberID must be empty, got %q", message.AuthorMemberID())
		}
	})

	t.Run("malformed workflow source does not replace Human", func(t *testing.T) {
		for name, source := range map[string]ContributionSource{
			"missing event id":  {Kind: ContributionSourceKindWorkflowRun, RefType: ContributionSourceRefTypeWorkflowRun, RefID: "run_1", RunID: "run_1"},
			"missing ref id":    {Kind: ContributionSourceKindWorkflowRun, RefType: ContributionSourceRefTypeWorkflowRun, RunID: "run_1", EventID: "wrf_1"},
			"ref run mismatch":  {Kind: ContributionSourceKindWorkflowRun, RefType: ContributionSourceRefTypeWorkflowRun, RefID: "run_1", RunID: "run_other", EventID: "wrf_1"},
			"wrong ref type":    {Kind: ContributionSourceKindWorkflowRun, RefType: ContributionSourceRefTypeMessage, RefID: "run_1", RunID: "run_1", EventID: "wrf_1"},
			"schedule run-only": {Kind: ContributionSourceKindScheduleRun, RefType: ContributionSourceRefTypeAutomationRun, RunID: "run_2"},
		} {
			t.Run(name, func(t *testing.T) {
				message := AppMessage{
					Source: source,
					AuthorProfile: AuthorProfile{
						ActorType:   AuthorActorTypeHuman,
						PrincipalID: "usr_owner",
						MemberID:    "mem_owner",
						DisplayName: "Owner",
					},
				}
				ApplyAutomationServiceAuthorToAppMessage(&message)
				if message.AuthorActorType() != AuthorActorTypeHuman {
					t.Errorf("malformed-source message actor type changed to %q", message.AuthorActorType())
				}
				if message.AuthorMemberID() != "mem_owner" {
					t.Errorf("malformed-source message lost the frozen Human member id")
				}
			})
		}
	})

	t.Run("ordinary human message is untouched", func(t *testing.T) {
		message := AppMessage{
			Source: ContributionSource{
				Kind:    ContributionSourceKindHumanCompose,
				RefType: ContributionSourceRefTypeMessage,
				RefID:   "msg_1",
			},
			AuthorProfile: AuthorProfile{
				ActorType:   AuthorActorTypeHuman,
				PrincipalID: "usr_owner",
				MemberID:    "mem_owner",
				DisplayName: "Owner",
			},
		}
		ApplyAutomationServiceAuthorToAppMessage(&message)
		if message.AuthorActorType() != AuthorActorTypeHuman {
			t.Errorf("ordinary human message actor type changed to %q", message.AuthorActorType())
		}
	})

	t.Run("agent message is untouched", func(t *testing.T) {
		message := AppMessage{
			Source: validWorkflowSource(),
			AuthorProfile: AuthorProfile{
				ActorType:   AuthorActorTypeAgent,
				PrincipalID: "agt_1",
				MemberID:    "mem_agent",
				DisplayName: "AgentTeam",
			},
		}
		ApplyAutomationServiceAuthorToAppMessage(&message)
		if message.AuthorActorType() != AuthorActorTypeAgent {
			t.Errorf("agent message actor type changed to %q", message.AuthorActorType())
		}
	})

	t.Run("already-service automation author is preserved", func(t *testing.T) {
		message := AppMessage{
			Source: validWorkflowSource(),
			AuthorProfile: AuthorProfile{
				ActorType:   AuthorActorTypeService,
				ServiceKind: AuthorServiceKindWorkflow,
				DisplayName: "Calculator",
			},
		}
		ApplyAutomationServiceAuthorToAppMessage(&message)
		if message.AuthorDisplayName() != "Calculator" {
			t.Errorf("service automation author was rewritten to %q", message.AuthorDisplayName())
		}
	})
}

func TestApplyAutomationServiceAuthorToHistoryMessage(t *testing.T) {
	t.Run("human-owned workflow row becomes Workflow author", func(t *testing.T) {
		message := HistoryMessage{
			Source: validWorkflowSource(),
			AuthorProfile: AuthorProfile{
				ActorType:   AuthorActorTypeHuman,
				MemberID:    "mem_owner",
				DisplayName: "Owner",
			},
		}
		ApplyAutomationServiceAuthorToHistoryMessage(&message)
		if message.AuthorProfile.ActorType != AuthorActorTypeService {
			t.Fatalf("AuthorProfile.ActorType = %q, want service", message.AuthorProfile.ActorType)
		}
		if message.AuthorProfile.ServiceKind != AuthorServiceKindWorkflow {
			t.Errorf("ServiceKind = %q, want workflow", message.AuthorProfile.ServiceKind)
		}
	})

	t.Run("malformed schedule source does not replace Human", func(t *testing.T) {
		message := HistoryMessage{
			Source: ContributionSource{
				Kind:    ContributionSourceKindScheduleRun,
				RefType: ContributionSourceRefTypeAutomationRun,
				RunID:   "run_2",
				Label:   "Daily standup",
			},
			AuthorProfile: AuthorProfile{
				ActorType:   AuthorActorTypeHuman,
				MemberID:    "mem_owner",
				DisplayName: "Owner",
			},
		}
		ApplyAutomationServiceAuthorToHistoryMessage(&message)
		if message.AuthorProfile.ActorType != AuthorActorTypeHuman {
			t.Errorf("malformed-source history author changed to %q", message.AuthorProfile.ActorType)
		}
	})
}

func TestApplyAutomationServiceAuthorToReceiveMessage(t *testing.T) {
	t.Run("human-owned workflow row becomes Workflow author", func(t *testing.T) {
		message := ReceiveMessage{
			Source: validWorkflowSource(),
			AuthorProfile: AuthorProfile{
				ActorType:   AuthorActorTypeHuman,
				MemberID:    "mem_owner",
				DisplayName: "Owner",
			},
		}
		ApplyAutomationServiceAuthorToReceiveMessage(&message)
		if message.AuthorProfile.ActorType != AuthorActorTypeService {
			t.Fatalf("AuthorProfile.ActorType = %q, want service", message.AuthorProfile.ActorType)
		}
		if message.AuthorProfile.ServiceKind != AuthorServiceKindWorkflow {
			t.Errorf("ServiceKind = %q, want workflow", message.AuthorProfile.ServiceKind)
		}
	})

	t.Run("malformed workflow source does not replace Human", func(t *testing.T) {
		message := ReceiveMessage{
			Source: ContributionSource{
				Kind:    ContributionSourceKindWorkflowRun,
				RefType: ContributionSourceRefTypeWorkflowRun,
				RefID:   "run_1",
				RunID:   "run_1",
			},
			AuthorProfile: AuthorProfile{
				ActorType:   AuthorActorTypeHuman,
				MemberID:    "mem_owner",
				DisplayName: "Owner",
			},
		}
		ApplyAutomationServiceAuthorToReceiveMessage(&message)
		if message.AuthorProfile.ActorType != AuthorActorTypeHuman {
			t.Errorf("malformed-source receive author changed to %q", message.AuthorProfile.ActorType)
		}
	})
}

func TestResolverOverlayAppliesAutomationServiceAuthorToAppMessage(t *testing.T) {
	queryer := &fakeContributionSourceQueryer{
		defaultRows: &fakeContributionSourceRows{},
	}
	resolver := NewContributionSourceResolver(queryer)

	message := AppMessage{
		ID: "msg_workflow",
		AuthorProfile: AuthorProfile{
			ActorType:   AuthorActorTypeHuman,
			PrincipalID: "usr_owner",
			MemberID:    "mem_owner",
			DisplayName: "Arithmetic E2E Owner",
		},
	}
	messages := []AppMessage{message}

	// The workflow_run source is not resolvable through the fake queryer's
	// native ledgers, so the resolver attaches the author-derived default source
	// and the service overlay must not rewrite an ordinary message.
	if err := resolver.AttachToAppMessages(context.Background(), "wsp_1", messages); err != nil {
		t.Fatalf("AttachToAppMessages: %v", err)
	}
	if messages[0].AuthorActorType() != AuthorActorTypeHuman {
		t.Errorf("ordinary human message actor type changed to %q", messages[0].AuthorActorType())
	}
}

func TestResolverOverlayPreservesAutomationServiceAuthor(t *testing.T) {
	queryer := &fakeContributionSourceQueryer{
		defaultRows: &fakeContributionSourceRows{},
	}
	resolver := NewContributionSourceResolver(queryer)

	message := AppMessage{
		ID: "msg_service_author",
		AuthorProfile: AuthorProfile{
			ActorType:   AuthorActorTypeService,
			ServiceKind: AuthorServiceKindWorkflow,
			DisplayName: "Parallel calculator",
		},
	}
	messages := []AppMessage{message}

	if err := resolver.AttachToAppMessages(context.Background(), "wsp_1", messages); err != nil {
		t.Fatalf("AttachToAppMessages: %v", err)
	}
	if messages[0].AuthorActorType() != AuthorActorTypeService {
		t.Errorf("service automation author was rewritten to %q", messages[0].AuthorActorType())
	}
	if messages[0].AuthorDisplayName() != "Parallel calculator" {
		t.Errorf("service automation author name changed to %q", messages[0].AuthorDisplayName())
	}
}
