package incident

import (
	"context"
	"strings"
	"testing"
	"time"

	gojira "github.com/andygrunwald/go-jira"
	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	coreuser "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	"github.com/tidbcloud/devops/entity"
	jiraent "github.com/tidbcloud/devops/entity/jira"
	"github.com/tidbcloud/devops/models"
	"github.com/tidbcloud/devops/pkg/infra/clients/jiracli"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli/notify"
	larkutil "github.com/tidbcloud/devops/pkg/infra/clients/larkcli/util"
	"github.com/tidbcloud/devops/pkg/infra/clients/opscore"
	"github.com/tidbcloud/devops/pkg/infra/config"
	testutil "github.com/tidbcloud/devops/pkg/infra/utils/test"
	svcbase "github.com/tidbcloud/devops/pkg/service"
	jirasvcapi "github.com/tidbcloud/devops/pkg/service/jira/api"
	"go.uber.org/mock/gomock"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func getUserRespForIncidentTest(email string) *userv1.GetUserResponse {
	return &userv1.GetUserResponse{User: &coreuser.User{Email: email}}
}

func listUsersRespForIncidentTest(emails ...string) *userv1.ListUsersResponse {
	resp := &userv1.ListUsersResponse{Users: make([]*coreuser.User, 0, len(emails)), Total: int64(len(emails))}
	for _, email := range emails {
		resp.Users = append(resp.Users, &coreuser.User{Email: email})
	}
	return resp
}

func TestNormalizeToDoActionStatus(t *testing.T) {
	cases := []struct {
		name       string
		input      *string
		useDefault bool
		want       *string
		wantErr    bool
	}{
		{name: "nil with default", input: nil, useDefault: true, want: strPtr(ToDoActionStatusOpen)},
		{name: "nil without default", input: nil, useDefault: false, want: nil},
		{name: "open", input: strPtr("open"), want: strPtr(ToDoActionStatusOpen)},
		{name: "ongoing legacy", input: strPtr("ongoing"), want: strPtr(ToDoActionStatusInProgress)},
		{name: "in progress display", input: strPtr("In Progress"), want: strPtr(ToDoActionStatusInProgress)},
		{name: "done", input: strPtr("done"), want: strPtr(ToDoActionStatusDone)},
		{name: "noneed", input: strPtr("noneed"), want: strPtr(ToDoActionStatusNoNeed)},
		{name: "canceled is not an action status", input: strPtr("canceled"), wantErr: true},
		{name: "cancelled is not an action status", input: strPtr("cancelled"), wantErr: true},
		{name: "invalid", input: strPtr("blocked"), wantErr: true},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			got, err := normalizeToDoActionStatus(tt.input, tt.useDefault)
			if tt.wantErr {
				if err == nil {
					t.Fatalf("expected error")
				}
				return
			}
			if err != nil {
				t.Fatalf("expected no error, got %v", err)
			}
			if tt.want == nil {
				if got != nil {
					t.Fatalf("expected nil, got %q", *got)
				}
				return
			}
			if got == nil || *got != *tt.want {
				t.Fatalf("expected %q, got %v", *tt.want, got)
			}
		})
	}
}

func TestParseExtractedPostMortemActions(t *testing.T) {
	raw := "```json\n[{\"title\":\"Fix alert\",\"owner_emails\":[\"owner@example.com\"],\"priority\":\"P1\",\"status\":\"open\"}]\n```"

	actions, err := parseExtractedPostMortemActions(raw)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(actions) != 1 {
		t.Fatalf("expected 1 action, got %d", len(actions))
	}
	if actions[0].Title != "Fix alert" {
		t.Fatalf("expected title Fix alert, got %q", actions[0].Title)
	}
	if actions[0].Priority != "P1" {
		t.Fatalf("expected priority P1, got %q", actions[0].Priority)
	}
	if len(actions[0].OwnerEmails) != 1 || actions[0].OwnerEmails[0] != "owner@example.com" {
		t.Fatalf("unexpected owner emails: %v", actions[0].OwnerEmails)
	}
}

func strPtr(s string) *string {
	return &s
}

func TestUniqueNonEmptyStrings(t *testing.T) {
	got := uniqueNonEmptyStrings([]string{" owner@example.com ", "", "owner@example.com", "other@example.com"})
	if len(got) != 2 || got[0] != "owner@example.com" || got[1] != "other@example.com" {
		t.Fatalf("unexpected unique strings: %v", got)
	}
}

func TestBuildOverdueToDoActionReminderParam(t *testing.T) {
	incidentID := uint64(30006)
	deadline := time.Date(2026, 5, 20, 8, 30, 0, 0, time.UTC)
	status := ToDoActionStatusInProgress
	title := "Fix alert"
	linkedJiraIssue := "TCOCP-123"
	svc := &incidentService{
		conf: config.IncidentServiceConfig{
			OpsPortalBaseURL: "https://ops-dev.tidbcloud.com/",
		},
	}

	param := svc.buildOverdueToDoActionReminderParam(&models.IncidentPostMortemToDoAction{
		IncidentId:      &incidentID,
		Title:           &title,
		Status:          &status,
		Deadline:        &deadline,
		LinkedJiraIssue: &linkedJiraIssue,
	}, &models.Incident{
		Updatable: models.Updatable{Base: models.Base{ID: models.ID(incidentID)}},
		Name:      "Incident 30006",
	}, "owner@example.com")
	msg, err := param.Message(notify.MessageCardSet{IncidentToDoActionOverdue: "owner-template"})
	if err != nil {
		t.Fatalf("build card failed: %v", err)
	}
	content, err := larkutil.EncodeIMMessage(msg)
	if err != nil {
		t.Fatalf("encode card failed: %v", err)
	}

	for _, want := range []string{
		"owner-template",
		"Fix alert",
		"in_progress",
		"2026-05-20 16:30",
		"Incident 30006",
		"owner@example.com",
		"https://ops-dev.tidbcloud.com/incidents/30006",
		"TCOCP-123",
		"https://tidb.atlassian.net/browse/TCOCP-123",
	} {
		if !strings.Contains(content, want) {
			t.Fatalf("expected reminder card to contain %q, got %q", want, content)
		}
	}
}

func TestBuildTCOCPCreatedParam(t *testing.T) {
	incidentID := uint64(30006)
	deadline := time.Date(2026, 6, 9, 8, 30, 0, 0, time.UTC)
	title := "Add healthcheck"
	svc := &incidentService{
		conf: config.IncidentServiceConfig{
			OpsPortalBaseURL: "https://ops-dev.tidbcloud.com/",
		},
	}

	param := svc.buildTCOCPCreatedParam(&entity.IncidentPostMortemToDoAction{
		IncidentId: &incidentID,
		Title:      &title,
		Deadline:   &deadline,
	}, &models.Incident{
		Updatable: models.Updatable{Base: models.Base{ID: models.ID(incidentID)}},
		Name:      "Incident 30006",
	}, &tcocpIssue{
		Key:  "TCOCP-543",
		Link: "https://tidb.atlassian.net/browse/TCOCP-543",
	}, "owner@example.com")
	msg, err := param.Message(notify.MessageCardSet{IncidentToDoActionTCOCPCreated: "created-template"})
	if err != nil {
		t.Fatalf("build card failed: %v", err)
	}
	content, err := larkutil.EncodeIMMessage(msg)
	if err != nil {
		t.Fatalf("encode card failed: %v", err)
	}

	for _, want := range []string{
		"created-template",
		"30006 - Incident 30006",
		"https://ops-dev.tidbcloud.com/incidents/30006",
		"Add healthcheck",
		"2026-06-09 16:30",
		"https://tidb.atlassian.net/browse/TCOCP-543",
		"TCOCP-543",
		"owner@example.com",
	} {
		if !strings.Contains(content, want) {
			t.Fatalf("expected TCOCP created card to contain %q, got %q", want, content)
		}
	}
}

func TestEnsureTCOCPIssueForToDoActionReusesExistingWithoutCreatedFlag(t *testing.T) {
	raw := "https://tidb.atlassian.net/browse/TCOCP-123"
	svc := &incidentService{}

	tcocp, err := svc.ensureTCOCPIssueForToDoAction(context.Background(), &entity.IncidentPostMortemToDoAction{}, nil, &raw)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if tcocp == nil {
		t.Fatalf("expected TCOCP issue")
	}
	if tcocp.Key != "TCOCP-123" || tcocp.Link != "https://tidb.atlassian.net/browse/TCOCP-123" {
		t.Fatalf("unexpected TCOCP issue: %#v", tcocp)
	}
	if tcocp.Created {
		t.Fatalf("existing TCOCP should not be marked as newly created")
	}
}

func TestEnsureImportedToDoActionTCOCPLinkSyncsExistingTCOCPFields(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	gdb, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		t.Fatalf("open sqlite db failed: %v", err)
	}
	if err := gdb.Exec(`CREATE TABLE incidents (id integer primary key, name text, jira_issues text)`).Error; err != nil {
		t.Fatalf("create incidents table failed: %v", err)
	}
	if err := gdb.Exec(`CREATE TABLE incident_postmortem_todoaction (id integer primary key, incident_id integer, persons text, deadline timestamp, linked_jira_issue text, priority text, status text, description text, tcocp_created_at timestamp, created_at timestamp, updated_at timestamp)`).Error; err != nil {
		t.Fatalf("create todoaction table failed: %v", err)
	}

	incidentID := uint64(30011)
	actionID := uint64(1001)
	defaultStatus := ToDoActionStatusOpen
	linkedJiraIssue := "https://tidb.atlassian.net/browse/TCOCP-426"
	localCreatedAt := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC)
	tcocpCreatedAt := time.Date(2026, 6, 2, 11, 0, 0, 0, time.UTC)
	tcocpETA := time.Date(2026, 6, 5, 0, 0, 0, 0, time.UTC)
	tcocpETAUnix := tcocpETA.Unix()
	if err := gdb.Exec(`INSERT INTO incidents (id, name) VALUES (?, ?)`, incidentID, "Incident 30011").Error; err != nil {
		t.Fatalf("create incident failed: %v", err)
	}
	if err := gdb.Exec(`INSERT INTO incident_postmortem_todoaction (id, incident_id, linked_jira_issue, status, created_at) VALUES (?, ?, ?, ?, ?)`, actionID, incidentID, linkedJiraIssue, defaultStatus, localCreatedAt).Error; err != nil {
		t.Fatalf("create todo action failed: %v", err)
	}

	dbProvider := testutil.NewMockTransProvider(ctrl)
	dbProvider.EXPECT().ReadDB(gomock.Any()).AnyTimes().Return(gdb)
	dbProvider.EXPECT().WriteDB(gomock.Any()).AnyTimes().Return(gdb)

	jiraSvc := jirasvcapi.NewMockJiraServiceAPI(ctrl)
	jiraSvc.EXPECT().
		GetIssueFromJira(gomock.Any(), "TCOCP-426", gomock.Any()).
		Return(&jiraent.IssueInJira{
			Key:       "TCOCP-426",
			Summary:   "TCOCP summary for existing action",
			Status:    "DONE",
			Priority:  "P1",
			CreatedAt: tcocpCreatedAt.Unix(),
			ETA:       &tcocpETAUnix,
			Assignee: &jiraent.User{
				Email:       "owner@pingcap.com",
				DisplayName: "Owner",
			},
		}, nil)

	userSvc := opscore.NewMockUserServiceClient(ctrl)
	userSvc.EXPECT().
		GetUser(gomock.Any(), gomock.Any()).
		AnyTimes().
		Return(getUserRespForIncidentTest("owner@pingcap.com"), nil)

	svc := &incidentService{
		dbProvider: dbProvider,
		jiraSvc:    jiraSvc,
		userSvc:    userSvc,
	}
	action := &entity.IncidentPostMortemToDoAction{
		Id:              actionID,
		IncidentId:      &incidentID,
		LinkedJiraIssue: &linkedJiraIssue,
		Status:          &defaultStatus,
	}

	if err := svc.ensureImportedToDoActionTCOCPLink(ctx, action, "", &linkedJiraIssue); err != nil {
		t.Fatalf("ensure imported TCOCP link failed: %v", err)
	}
	if action.LinkedJiraIssue == nil || *action.LinkedJiraIssue != "TCOCP-426" {
		t.Fatalf("expected normalized TCOCP key, got %v", action.LinkedJiraIssue)
	}
	if action.Status == nil || *action.Status != ToDoActionStatusDone {
		t.Fatalf("expected action status synced to done, got %v", action.Status)
	}
	if len(action.Persons) != 1 || action.Persons[0].Email != "owner@pingcap.com" {
		t.Fatalf("expected action persons synced from TCOCP assignee, got %v", action.Persons)
	}
	if action.Deadline == nil || !action.Deadline.Equal(tcocpETA) {
		t.Fatalf("expected action deadline synced to %s, got %v", tcocpETA, action.Deadline)
	}
	if action.Priority == nil || *action.Priority != "P1" {
		t.Fatalf("expected action priority synced to P1, got %v", action.Priority)
	}
	if action.Description == nil || *action.Description != "TCOCP summary for existing action" {
		t.Fatalf("expected action description synced from TCOCP summary, got %v", action.Description)
	}
	if action.TCOCPCreatedAt == nil || !action.TCOCPCreatedAt.Equal(tcocpCreatedAt) {
		t.Fatalf("expected action TCOCP created at synced to %s, got %v", tcocpCreatedAt, action.TCOCPCreatedAt)
	}

	var got models.IncidentPostMortemToDoAction
	if err := gdb.Where("id = ?", actionID).First(&got).Error; err != nil {
		t.Fatalf("get todo action failed: %v", err)
	}
	if got.LinkedJiraIssue == nil || *got.LinkedJiraIssue != "TCOCP-426" {
		t.Fatalf("expected db linked jira issue TCOCP-426, got %v", got.LinkedJiraIssue)
	}
	if got.Status == nil || *got.Status != ToDoActionStatusDone {
		t.Fatalf("expected db status done, got %v", got.Status)
	}
	if got.Persons == nil || *got.Persons != "owner@pingcap.com" {
		t.Fatalf("expected db persons owner@pingcap.com, got %v", got.Persons)
	}
	if got.Deadline == nil || !got.Deadline.Equal(tcocpETA) {
		t.Fatalf("expected db deadline %s, got %v", tcocpETA, got.Deadline)
	}
	if got.Priority == nil || *got.Priority != "P1" {
		t.Fatalf("expected db priority P1, got %v", got.Priority)
	}
	if got.Description == nil || *got.Description != "TCOCP summary for existing action" {
		t.Fatalf("expected db description synced from TCOCP summary, got %v", got.Description)
	}
	if got.TCOCPCreatedAt == nil || !got.TCOCPCreatedAt.Equal(tcocpCreatedAt) {
		t.Fatalf("expected db TCOCP created at %s, got %v", tcocpCreatedAt, got.TCOCPCreatedAt)
	}
	if !got.CreatedAt.Equal(localCreatedAt) {
		t.Fatalf("expected local action created_at unchanged as %s, got %s", localCreatedAt, got.CreatedAt)
	}
}

func TestEnsureImportedToDoActionTCOCPLinkSyncsNewTCOCPFields(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	gdb, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		t.Fatalf("open sqlite db failed: %v", err)
	}
	if err := gdb.Exec(`CREATE TABLE incidents (id integer primary key, name text, jira_issues text)`).Error; err != nil {
		t.Fatalf("create incidents table failed: %v", err)
	}
	if err := gdb.Exec(`CREATE TABLE incident_postmortem_todoaction (id integer primary key, incident_id integer, persons text, deadline timestamp, linked_jira_issue text, priority text, status text, description text, tcocp_created_at timestamp, created_at timestamp, updated_at timestamp)`).Error; err != nil {
		t.Fatalf("create todoaction table failed: %v", err)
	}

	incidentID := uint64(30012)
	actionID := uint64(1002)
	workType := tcocpWorkTypeHealthcheck
	defaultStatus := ToDoActionStatusOpen
	localCreatedAt := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC)
	tcocpCreatedAt := time.Date(2026, 6, 3, 12, 0, 0, 0, time.UTC)
	tcocpETA := time.Date(2026, 6, 8, 0, 0, 0, 0, time.UTC)
	tcocpETAUnix := tcocpETA.Unix()
	if err := gdb.Exec(`INSERT INTO incidents (id, name) VALUES (?, ?)`, incidentID, "Incident 30012").Error; err != nil {
		t.Fatalf("create incident failed: %v", err)
	}
	if err := gdb.Exec(`INSERT INTO incident_postmortem_todoaction (id, incident_id, status, created_at) VALUES (?, ?, ?, ?)`, actionID, incidentID, defaultStatus, localCreatedAt).Error; err != nil {
		t.Fatalf("create todo action failed: %v", err)
	}

	dbProvider := testutil.NewMockTransProvider(ctrl)
	dbProvider.EXPECT().ReadDB(gomock.Any()).AnyTimes().Return(gdb)
	dbProvider.EXPECT().WriteDB(gomock.Any()).AnyTimes().Return(gdb)

	jiraCli := jiracli.NewMockJiraClient(ctrl)
	jiraCli.EXPECT().
		CreateIssue(gomock.Any(), gomock.Any()).
		Return(&gojira.Issue{Key: "TCOCP-777"}, nil)

	jiraSvc := jirasvcapi.NewMockJiraServiceAPI(ctrl)
	jiraSvc.EXPECT().
		GetIssueFromJira(gomock.Any(), "TCOCP-777", gomock.Any()).
		Return(&jiraent.IssueInJira{
			Key:       "TCOCP-777",
			Summary:   "TCOCP summary for new action",
			Status:    "IN PROGRESS",
			Priority:  tcocpDefaultPriority,
			CreatedAt: tcocpCreatedAt.Unix(),
			ETA:       &tcocpETAUnix,
			Assignee: &jiraent.User{
				Email:       "new-owner@pingcap.com",
				DisplayName: "New Owner",
			},
		}, nil)

	userSvc := opscore.NewMockUserServiceClient(ctrl)
	userSvc.EXPECT().
		GetUser(gomock.Any(), gomock.Any()).
		AnyTimes().
		Return(getUserRespForIncidentTest("new-owner@pingcap.com"), nil)

	svc := &incidentService{
		dbProvider: dbProvider,
		jiraCli:    jiraCli,
		jiraSvc:    jiraSvc,
		userSvc:    userSvc,
	}
	action := &entity.IncidentPostMortemToDoAction{
		Id:         actionID,
		IncidentId: &incidentID,
		Title:      strPtr("Add healthcheck"),
		Type:       &workType,
		Status:     &defaultStatus,
	}

	if err := svc.ensureImportedToDoActionTCOCPLink(ctx, action, "", nil); err != nil {
		t.Fatalf("ensure imported TCOCP link failed: %v", err)
	}
	if action.LinkedJiraIssue == nil || *action.LinkedJiraIssue != "TCOCP-777" {
		t.Fatalf("expected normalized TCOCP key, got %v", action.LinkedJiraIssue)
	}
	if action.Status == nil || *action.Status != ToDoActionStatusInProgress {
		t.Fatalf("expected action status synced to in_progress, got %v", action.Status)
	}
	if len(action.Persons) != 1 || action.Persons[0].Email != "new-owner@pingcap.com" {
		t.Fatalf("expected action persons synced from new TCOCP assignee, got %v", action.Persons)
	}
	if action.Deadline == nil || !action.Deadline.Equal(tcocpETA) {
		t.Fatalf("expected action deadline synced to %s, got %v", tcocpETA, action.Deadline)
	}
	if action.Priority == nil || *action.Priority != tcocpDefaultPriority {
		t.Fatalf("expected action priority synced to %s, got %v", tcocpDefaultPriority, action.Priority)
	}
	if action.Description == nil || *action.Description != "TCOCP summary for new action" {
		t.Fatalf("expected action description synced from TCOCP summary, got %v", action.Description)
	}
	if action.TCOCPCreatedAt == nil || !action.TCOCPCreatedAt.Equal(tcocpCreatedAt) {
		t.Fatalf("expected action TCOCP created at synced to %s, got %v", tcocpCreatedAt, action.TCOCPCreatedAt)
	}

	var got models.IncidentPostMortemToDoAction
	if err := gdb.Where("id = ?", actionID).First(&got).Error; err != nil {
		t.Fatalf("get todo action failed: %v", err)
	}
	if got.LinkedJiraIssue == nil || *got.LinkedJiraIssue != "TCOCP-777" {
		t.Fatalf("expected db linked jira issue TCOCP-777, got %v", got.LinkedJiraIssue)
	}
	if got.Status == nil || *got.Status != ToDoActionStatusInProgress {
		t.Fatalf("expected db status in_progress, got %v", got.Status)
	}
	if got.Persons == nil || *got.Persons != "new-owner@pingcap.com" {
		t.Fatalf("expected db persons new-owner@pingcap.com, got %v", got.Persons)
	}
	if got.Deadline == nil || !got.Deadline.Equal(tcocpETA) {
		t.Fatalf("expected db deadline %s, got %v", tcocpETA, got.Deadline)
	}
	if got.Priority == nil || *got.Priority != tcocpDefaultPriority {
		t.Fatalf("expected db priority %s, got %v", tcocpDefaultPriority, got.Priority)
	}
	if got.Description == nil || *got.Description != "TCOCP summary for new action" {
		t.Fatalf("expected db description synced from TCOCP summary, got %v", got.Description)
	}
	if got.TCOCPCreatedAt == nil || !got.TCOCPCreatedAt.Equal(tcocpCreatedAt) {
		t.Fatalf("expected db TCOCP created at %s, got %v", tcocpCreatedAt, got.TCOCPCreatedAt)
	}
	if !got.CreatedAt.Equal(localCreatedAt) {
		t.Fatalf("expected local action created_at unchanged as %s, got %s", localCreatedAt, got.CreatedAt)
	}
}

func TestBuildIncidentOverdueToDoActionSummaryParam(t *testing.T) {
	incidentID := uint64(30006)
	deadline := time.Date(2026, 5, 20, 8, 30, 0, 0, time.UTC)
	status := ToDoActionStatusOpen
	title := "Fix alert"
	persons := "owner@example.com other@example.com"
	linkedJiraIssue := "TCOCP-456"
	svc := &incidentService{
		conf: config.IncidentServiceConfig{
			OpsPortalBaseURL: "https://ops-dev.tidbcloud.com/",
		},
	}

	param := svc.buildIncidentOverdueToDoActionSummaryParam(&models.Incident{
		Updatable: models.Updatable{Base: models.Base{ID: models.ID(incidentID)}},
		Name:      "Incident 30006",
	}, []*models.IncidentPostMortemToDoAction{
		{
			IncidentId:      &incidentID,
			Title:           &title,
			Status:          &status,
			Deadline:        &deadline,
			Persons:         &persons,
			LinkedJiraIssue: &linkedJiraIssue,
		},
	})
	msg, err := param.Message(notify.MessageCardSet{IncidentToDoActionOverdueSummary: "summary-template"})
	if err != nil {
		t.Fatalf("build card failed: %v", err)
	}
	content, err := larkutil.EncodeIMMessage(msg)
	if err != nil {
		t.Fatalf("encode card failed: %v", err)
	}

	for _, want := range []string{
		"summary-template",
		"1",
		"Incident 30006",
		"https://ops-dev.tidbcloud.com/incidents/30006",
		"**1. Fix alert**",
		"TCOCP-456",
		"https://tidb.atlassian.net/browse/TCOCP-456",
		"owner@example.com, other@example.com",
	} {
		if !strings.Contains(content, want) {
			t.Fatalf("expected summary card to contain %q, got %q", want, content)
		}
	}
	if !strings.Contains(param.ActionItems, "Status: open &nbsp;|&nbsp; Due: 2026-05-20 16:30") {
		t.Fatalf("unexpected action_items: %q", param.ActionItems)
	}
	for _, unwanted := range []string{"action_title", "status", "due_time", "owner_email", "feishu_task_link"} {
		if strings.Contains(content, unwanted) {
			t.Fatalf("summary card should not contain variable %q, got %q", unwanted, content)
		}
	}
}

func TestParseLinkedJiraIssues(t *testing.T) {
	raw := "https://tidb.atlassian.net/browse/abc-1\nTCOC-2184, https://tidb.atlassian.net/browse/TCOCP-123, TCOCP-123, DEF-2"
	got := parseLinkedJiraIssues(&raw)
	if len(got.TCOCP) != 1 || got.TCOCP[0] != "TCOCP-123" {
		t.Fatalf("unexpected TCOCP issues: %v", got.TCOCP)
	}
	if len(got.TCOC) != 1 || got.TCOC[0] != "TCOC-2184" {
		t.Fatalf("unexpected TCOC issues: %v", got.TCOC)
	}
	if len(got.NonTCOCP) != 2 || got.NonTCOCP[0] != "ABC-1" || got.NonTCOCP[1] != "DEF-2" {
		t.Fatalf("unexpected non-TCOCP issues: %v", got.NonTCOCP)
	}
}

func TestMapTCOCPStatusToToDoActionStatus(t *testing.T) {
	got, ok := mapTCOCPStatusToToDoActionStatus("In Progress")
	if !ok || got != ToDoActionStatusInProgress {
		t.Fatalf("expected in_progress, got %q ok=%v", got, ok)
	}

	if got, ok := mapTCOCPStatusToToDoActionStatus("Waiting for customer"); ok || got != "" {
		t.Fatalf("expected unknown status to be skipped, got %q ok=%v", got, ok)
	}

	got, ok = mapTCOCPStatusToToDoActionStatus("NONEED")
	if !ok || got != ToDoActionStatusNoNeed {
		t.Fatalf("expected noneed, got %q ok=%v", got, ok)
	}
}

func TestBuildTCOCPIssue(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	deadline := time.Date(2026, 6, 3, 8, 30, 0, 0, time.UTC)
	workType := tcocpWorkTypeHealthcheck
	priority := "P1"
	ownerEmail := "owner@example.com"
	jiraCli := jiracli.NewMockJiraClient(ctrl)
	jiraCli.EXPECT().
		FindUsers(gomock.Any(), ownerEmail, 5).
		Return([]gojira.User{{AccountID: "account-1", EmailAddress: ownerEmail, AccountType: "atlassian"}}, nil)
	jiraCli.EXPECT().
		FindAssignableUsers(gomock.Any(), tcocpProjectKey, "account-1", 5).
		Return([]gojira.User{{AccountID: "account-1"}}, nil)

	svc := &incidentService{
		conf: config.IncidentServiceConfig{
			OpsPortalBaseURL: "https://ops-dev.tidbcloud.com/",
		},
		jiraCli: jiraCli,
	}
	incidentID := uint64(30006)
	postmortemURL := "https://tidb.feishu.cn/docx/postmortem"
	incidentJiraIssues := "TCOC-2184 ST-1"
	issue, err := svc.buildTCOCPIssue(context.Background(), &entity.IncidentPostMortemToDoAction{
		IncidentId:  &incidentID,
		Title:       strPtr("Add healthcheck"),
		Description: strPtr("Add a daily healthcheck"),
		Type:        &workType,
		Priority:    &priority,
		Deadline:    &deadline,
		Persons: []*coreuser.User{
			{Email: ownerEmail},
		},
	}, &models.Incident{
		Updatable:   models.Updatable{Base: models.Base{ID: models.ID(incidentID)}},
		Name:        "Incident 30006",
		FeishuLinks: &postmortemURL,
		JiraIssues:  &incidentJiraIssues,
	}, strPtr("ST-1"))
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if issue.Fields.Project.Key != tcocpProjectKey || issue.Fields.Type.Name != workType {
		t.Fatalf("unexpected project/type: %#v", issue.Fields)
	}
	if issue.Fields.Assignee == nil || issue.Fields.Assignee.AccountID != "account-1" {
		t.Fatalf("unexpected assignee: %#v", issue.Fields.Assignee)
	}
	if issue.Fields.Priority == nil || issue.Fields.Priority.Name != "P1" {
		t.Fatalf("unexpected priority: %#v", issue.Fields.Priority)
	}
	if issue.Fields.Unknowns[tcocpFieldETA] != "2026-06-03" {
		t.Fatalf("unexpected ETA: %#v", issue.Fields.Unknowns)
	}
	if issue.Fields.Unknowns[tcocpFieldSyncToONCALL].(map[string]string)["value"] != "yes" {
		t.Fatalf("unexpected oncall field: %#v", issue.Fields.Unknowns)
	}
	if issue.Fields.Unknowns[tcocpFieldFromTCOC].(map[string]string)["value"] != "Yes" {
		t.Fatalf("unexpected from TCOC field: %#v", issue.Fields.Unknowns)
	}
	if !strings.Contains(strings.Join(issue.Fields.Labels, ","), "incident-30006") {
		t.Fatalf("unexpected labels: %#v", issue.Fields.Labels)
	}
	if !strings.Contains(issue.Fields.Description, "https://ops-dev.tidbcloud.com/incidents/30006") {
		t.Fatalf("expected incident link in description, got %q", issue.Fields.Description)
	}
}

func TestBuildTCOCPIssueSkipsUnassignableAssignee(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	workType := tcocpWorkTypeHealthcheck
	ownerEmail := "owner@example.com"
	jiraCli := jiracli.NewMockJiraClient(ctrl)
	jiraCli.EXPECT().
		FindUsers(gomock.Any(), ownerEmail, 5).
		Return([]gojira.User{{AccountID: "account-1", AccountType: "atlassian"}}, nil)
	jiraCli.EXPECT().
		FindAssignableUsers(gomock.Any(), tcocpProjectKey, "account-1", 5).
		Return([]gojira.User{}, nil)

	svc := &incidentService{
		JobService: svcbase.NewJobService("IncidentService"),
		jiraCli:    jiraCli,
	}
	issue, err := svc.buildTCOCPIssue(context.Background(), &entity.IncidentPostMortemToDoAction{
		Title: strPtr("Add healthcheck"),
		Type:  &workType,
		Persons: []*coreuser.User{
			{Email: ownerEmail},
		},
	}, nil, nil)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if issue.Fields.Assignee != nil {
		t.Fatalf("assignee should be skipped when Jira user is not assignable: %#v", issue.Fields.Assignee)
	}
}

func TestSelectJiraUserForEmailPrefersAtlassianAccount(t *testing.T) {
	got := selectJiraUserForEmail([]gojira.User{
		{
			AccountID:    "qm:customer-account",
			AccountType:  "customer",
			EmailAddress: "xieyujie@pingcap.com",
		},
		{
			AccountID:    "712020:1c202eaf-71cf-4ec1-85be-c1fbed21e15a",
			AccountType:  "atlassian",
			EmailAddress: "xieyujie@pingcap.com",
		},
	}, "xieyujie@pingcap.com")
	if got == nil || got.AccountID != "712020:1c202eaf-71cf-4ec1-85be-c1fbed21e15a" {
		t.Fatalf("expected atlassian account, got %#v", got)
	}
}

func TestBuildTCOCPIssueSkipsFromTCOCWithoutTCOCIssue(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	workType := tcocpWorkTypeHealthcheck
	jiraCli := jiracli.NewMockJiraClient(ctrl)

	svc := &incidentService{jiraCli: jiraCli}
	incidentID := uint64(30006)
	incidentJiraIssues := "ST-1"
	issue, err := svc.buildTCOCPIssue(context.Background(), &entity.IncidentPostMortemToDoAction{
		IncidentId: &incidentID,
		Title:      strPtr("Add healthcheck"),
		Type:       &workType,
	}, &models.Incident{
		Updatable:  models.Updatable{Base: models.Base{ID: models.ID(incidentID)}},
		Name:       "Incident 30006",
		JiraIssues: &incidentJiraIssues,
	}, strPtr("ST-1"))
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if _, ok := issue.Fields.Unknowns[tcocpFieldFromTCOC]; ok {
		t.Fatalf("from TCOC field should not be set without incident TCOC issue: %#v", issue.Fields.Unknowns)
	}
	if issue.Fields.Priority == nil || issue.Fields.Priority.Name != tcocpDefaultPriority {
		t.Fatalf("expected default priority %s, got %#v", tcocpDefaultPriority, issue.Fields.Priority)
	}
}

func TestLinkTCOCPToIncidentIssuesOnlyLinksTCOC(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	incidentJiraIssues := "TCOC-2184 ST-1 TCOCP-123"
	jiraCli := jiracli.NewMockJiraClient(ctrl)
	jiraCli.EXPECT().
		CreateIssueLink(gomock.Any(), tcocpIssueLinkAssociated, "TCOCP-543", "TCOC-2184").
		Return(nil)

	svc := &incidentService{jiraCli: jiraCli}
	err := svc.linkTCOCPToIncidentIssues(context.Background(), "TCOCP-543", &models.Incident{
		JiraIssues: &incidentJiraIssues,
	})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
}

func TestCreateIncident(t *testing.T) {

}

func TestUpdateIncident(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	gdb, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		t.Fatalf("open sqlite db failed: %v", err)
	}
	if err := gdb.Exec(`CREATE TABLE incidents (
		id integer primary key autoincrement,
		created_at datetime not null,
		updated_at datetime,
		name text not null default '',
		tier text,
		oncallers text,
		team_leaders text,
		fbowners text,
		status text not null default '',
		biz_region text not null default '',
		jira_issues text,
		feishu_links text,
		components text,
		description text not null default '',
		incident_start_time datetime,
		incident_resolve_time datetime,
		war_room_id integer,
		feishu_channel_msg text,
		postmortem_todo_action_import_status text not null default ''
	)`).Error; err != nil {
		t.Fatalf("create incidents table failed: %v", err)
	}

	dbProvider := testutil.NewMockTransProvider(ctrl)
	dbProvider.EXPECT().WriteDB(gomock.Any()).AnyTimes().Return(gdb)
	svc := &incidentService{dbProvider: dbProvider}

	warRoomID := uint64(100)
	incidentID, err := svc.CreateIncident(ctx, &entity.Incident{WarRoomID: &warRoomID})
	if err != nil {
		t.Fatalf("create incident failed: %v", err)
	}
	assertIncidentWarRoomID(t, gdb, incidentID, &warRoomID)

	if err := svc.UpdateIncident(ctx, incidentID, &entity.Incident{}); err != nil {
		t.Fatalf("update incident without war room ID failed: %v", err)
	}
	assertIncidentWarRoomID(t, gdb, incidentID, &warRoomID)

	updatedWarRoomID := uint64(200)
	if err := svc.UpdateIncident(ctx, incidentID, &entity.Incident{WarRoomID: &updatedWarRoomID}); err != nil {
		t.Fatalf("set incident war room ID failed: %v", err)
	}
	assertIncidentWarRoomID(t, gdb, incidentID, &updatedWarRoomID)

	nullIncidentID, err := svc.CreateIncident(ctx, &entity.Incident{})
	if err != nil {
		t.Fatalf("create incident with null war room ID failed: %v", err)
	}
	assertIncidentWarRoomID(t, gdb, nullIncidentID, nil)
}

func assertIncidentWarRoomID(t *testing.T, db *gorm.DB, incidentID uint64, want *uint64) {
	t.Helper()

	var incident models.Incident
	if err := db.Where("id = ?", incidentID).First(&incident).Error; err != nil {
		t.Fatalf("load incident failed: %v", err)
	}
	incidentEntity := (&incidentService{}).incidentModel2Entity(context.Background(), &incident, false)
	if want == nil {
		if incident.WarRoomID != nil || incidentEntity.WarRoomID != nil {
			t.Fatalf("expected null war room ID, got model=%#v entity=%#v", incident.WarRoomID, incidentEntity.WarRoomID)
		}
		return
	}
	if incident.WarRoomID == nil || *incident.WarRoomID != *want {
		t.Fatalf("expected war room ID %d, got %#v", *want, incident.WarRoomID)
	}
	if incidentEntity.WarRoomID == nil || *incidentEntity.WarRoomID != *want {
		t.Fatalf("expected entity war room ID %d, got %#v", *want, incidentEntity.WarRoomID)
	}
}

func TestGetIncident(t *testing.T) {

}

func TestDeleteIncident(t *testing.T) {

}

func TestListIncidents(t *testing.T) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	gdb, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		t.Fatalf("open sqlite db failed: %v", err)
	}
	if err := gdb.Exec(`CREATE TABLE incidents (
		id integer primary key autoincrement,
		created_at datetime not null,
		war_room_id integer,
		status text,
		tier text,
		components text,
		biz_region text
	)`).Error; err != nil {
		t.Fatalf("create incidents table failed: %v", err)
	}

	createdAt := time.Date(2026, 7, 21, 0, 0, 0, 0, time.UTC)
	for i := 1; i <= 55; i++ {
		var warRoomID any
		switch i {
		case 1, 2:
			warRoomID = uint64(99)
		case 3:
			warRoomID = uint64(0)
		}
		status := entity.IncidentStatusRunning
		if i%2 == 0 {
			status = entity.IncidentStatusFinished
		}
		tiers := []entity.IncidentTier{
			entity.IncidentTierT0,
			entity.IncidentTierT1,
			entity.IncidentTierT2,
			entity.IncidentTierT3,
			entity.IncidentTierT4,
		}
		tier := tiers[(i-1)%len(tiers)]
		components := "TiKV"
		switch i % 3 {
		case 0:
			components = "TiDB TiKV"
		case 1:
			components = "TiDB"
		}
		bizRegion := "NA"
		switch i % 4 {
		case 0:
			bizRegion = "APAC EMEA"
		case 1:
			bizRegion = "APAC"
		case 2:
			bizRegion = "EMEA"
		}
		if err := gdb.Exec(
			"INSERT INTO incidents (created_at, war_room_id, status, tier, components, biz_region) VALUES (?, ?, ?, ?, ?, ?)",
			createdAt,
			warRoomID,
			status,
			tier,
			components,
			bizRegion,
		).Error; err != nil {
			t.Fatalf("insert incident %d failed: %v", i, err)
		}
	}

	dbProvider := testutil.NewMockTransProvider(ctrl)
	dbProvider.EXPECT().ReadDB(gomock.Any()).AnyTimes().Return(gdb)
	svc := &incidentService{dbProvider: dbProvider}

	t.Run("uses default pagination", func(t *testing.T) {
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 55 {
			t.Fatalf("expected total 55, got %d", total)
		}
		if len(incidents) != 50 {
			t.Fatalf("expected 50 incidents, got %d", len(incidents))
		}
		if incidents[0].Id != 55 || incidents[49].Id != 6 {
			t.Fatalf("unexpected default page IDs: first=%d last=%d", incidents[0].Id, incidents[49].Id)
		}
	})

	t.Run("uses requested page", func(t *testing.T) {
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{Page: 2, PerPage: 10})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 55 {
			t.Fatalf("expected total 55, got %d", total)
		}
		if len(incidents) != 10 || incidents[0].Id != 45 || incidents[9].Id != 36 {
			t.Fatalf("unexpected second page: %#v", incidents)
		}
	})

	t.Run("filters by war room ID", func(t *testing.T) {
		warRoomID := uint64(99)
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{Page: 1, PerPage: 1, WarRoomID: &warRoomID})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 2 || len(incidents) != 1 {
			t.Fatalf("expected 1 incident and total 2, got len=%d total=%d", len(incidents), total)
		}
		if incidents[0].Id != 2 {
			t.Fatalf("unexpected filtered incident ID: %d", incidents[0].Id)
		}
	})

	t.Run("filters by zero war room ID", func(t *testing.T) {
		warRoomID := uint64(0)
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{WarRoomID: &warRoomID})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 1 || len(incidents) != 1 || incidents[0].Id != 3 {
			t.Fatalf("unexpected zero war room ID result: incidents=%#v total=%d", incidents, total)
		}
	})

	t.Run("filters by status", func(t *testing.T) {
		status := entity.IncidentStatusFinished
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{StatusIs: &status})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 27 || len(incidents) != 27 || incidents[0].Id != 54 {
			t.Fatalf("unexpected status filtered result: incidents=%#v total=%d", incidents, total)
		}
	})

	t.Run("filters by tier", func(t *testing.T) {
		tier := entity.IncidentTierT0
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{TierIs: &tier})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 11 || len(incidents) != 11 || incidents[0].Id != 51 {
			t.Fatalf("unexpected tier filtered result: incidents=%#v total=%d", incidents, total)
		}
	})

	t.Run("filters by one component", func(t *testing.T) {
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{
			ComponentsContain: []string{"TiDB"},
		})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 37 || len(incidents) != 37 || incidents[0].Id != 55 {
			t.Fatalf("unexpected component filtered result: incidents=%#v total=%d", incidents, total)
		}
	})

	t.Run("filters by every component", func(t *testing.T) {
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{
			Page:              2,
			PerPage:           5,
			ComponentsContain: []string{"TiDB", "TiKV"},
		})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 18 || len(incidents) != 5 || incidents[0].Id != 39 || incidents[4].Id != 27 {
			t.Fatalf("unexpected component filtered result: incidents=%#v total=%d", incidents, total)
		}
	})

	t.Run("filters by every business region", func(t *testing.T) {
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{
			BizRegionsContain: []string{"APAC", "EMEA"},
		})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 13 || len(incidents) != 13 || incidents[0].Id != 52 || incidents[12].Id != 4 {
			t.Fatalf("unexpected business region filtered result: incidents=%#v total=%d", incidents, total)
		}
	})

	t.Run("combines incident filters", func(t *testing.T) {
		status := entity.IncidentStatusRunning
		tier := entity.IncidentTierT0
		incidents, total, err := svc.ListIncidents(ctx, ListIncidentsParam{
			StatusIs:          &status,
			TierIs:            &tier,
			ComponentsContain: []string{"TiDB", "TiKV"},
			BizRegionsContain: []string{"APAC"},
		})
		if err != nil {
			t.Fatalf("list incidents failed: %v", err)
		}
		if total != 1 || len(incidents) != 1 || incidents[0].Id != 21 {
			t.Fatalf("unexpected combined filtered result: incidents=%#v total=%d", incidents, total)
		}
	})

}

func TestListIncidentLogs(t *testing.T) {

}

func TestGetIncidentLogItem(t *testing.T) {

}

func TestCreateIncidentLogItem(t *testing.T) {

}

func TestUpdateIncidentLogItem(t *testing.T) {

}

func TestDeleteIncidentLogItem(t *testing.T) {

}

func TestUpdateFeishuMessage(t *testing.T) {

}
