package incident

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

	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli/document"
	"github.com/tidbcloud/devops/pkg/infra/clients/opscore"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"go.uber.org/mock/gomock"
)

type fakeDocumentClient struct {
	wikiToken string
}

func (f fakeDocumentClient) GetDocumentByID(ctx context.Context, documentID string) error { return nil }

func (f fakeDocumentClient) GetDocumentPlainText(ctx context.Context, documentID string) (string, error) {
	return "", nil
}

func (f fakeDocumentClient) GetWikiDocumentToken(ctx context.Context, wikiToken string) (string, error) {
	return f.wikiToken, nil
}

func (f fakeDocumentClient) NewDefaultDocumentBuilder(docTitle string) (document.DocumentBuilder, error) {
	return nil, nil
}

func (f fakeDocumentClient) GenerateURLFromDocumentID(documentID string) string { return documentID }

func TestDocumentTokenFromFeishuURL(t *testing.T) {
	svc := &incidentService{
		larkclient: &larkcli.LarkClient{
			Document: fakeDocumentClient{wikiToken: "doc-token"},
		},
	}

	t.Run("docx", func(t *testing.T) {
		got, err := svc.documentTokenFromFeishuURL(context.Background(), "https://tidb.feishu.cn/docx/abc123")
		if err != nil {
			t.Fatalf("expected no error, got %v", err)
		}
		if got != "abc123" {
			t.Fatalf("expected abc123, got %q", got)
		}
	})

	t.Run("wiki", func(t *testing.T) {
		got, err := svc.documentTokenFromFeishuURL(context.Background(), "https://tidb.feishu.cn/wiki/zzz")
		if err != nil {
			t.Fatalf("expected no error, got %v", err)
		}
		if got != "doc-token" {
			t.Fatalf("expected doc-token, got %q", got)
		}
	})
}

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

	userSvc := opscore.NewMockUserServiceClient(ctrl)
	userSvc.EXPECT().
		ListUsers(gomock.Any(), gomock.Any()).
		Return(listUsersRespForIncidentTest("owner@example.com"), nil)

	svc := &incidentService{userSvc: userSvc}
	got := svc.resolveExtractedActionOwners(context.Background(), extractedPostMortemAction{
		OwnerEmails:      []string{"", "owner@example.com", "owner@example.com"},
		OwnerLarkUserIDs: []string{"@lark_user_id:ou_123"},
		Description:      "Fix alert subscription @lark_user_id:ou_456",
	})

	if len(got) != 1 || got[0] != "owner@example.com" {
		t.Fatalf("unexpected owners: %v", got)
	}
}

func TestParseExtractedDeadline(t *testing.T) {
	got, err := parseExtractedDeadline("2026-05-20")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if got == nil || got.Format("2006-01-02") != "2026-05-20" {
		t.Fatalf("unexpected deadline: %v", got)
	}

	if got, err := parseExtractedDeadline(""); err != nil || got != nil {
		t.Fatalf("expected nil deadline, got %v err %v", got, err)
	}
}

func TestPostMortemActionExtractionPromptUsesOneActionPerRow(t *testing.T) {
	prompt := buildPostMortemActionExtractionPrompt("Action Items")
	for _, want := range []string{
		"produce exactly one JSON object per data row",
		"Do not split one table row because the Item cell contains multiple lines",
		"Preserve Item-cell line boundaries in description",
		"format each sub-item on its own line",
		"Do not collapse the end of one sub-item and the beginning of the next sub-item into a single space-separated sentence",
		"owner_emails and owner_lark_user_ids must come only from the same row's Assignee cell",
		"Mentions inside the Item cell are content only",
	} {
		if !strings.Contains(prompt, want) {
			t.Fatalf("expected prompt to contain %q", want)
		}
	}
	for _, unwanted := range []string{
		"split them into multiple JSON objects",
		"every split action",
		"Mentions inside a specific Item line also apply",
	} {
		if strings.Contains(prompt, unwanted) {
			t.Fatalf("expected prompt not to contain old split rule %q", unwanted)
		}
	}
}

func TestParseExtractedPostMortemActionsDoesNotSplitRowLevelAction(t *testing.T) {
	raw := `[
  {
    "title": "故障响应流程优化",
    "description": "故障响应流程优化：\nWar room 相关机制配套功能 @lark_user_id:g77bf2e1\n订阅AWS,GCP alerts @lark_user_id:g77bf2e1\n@lark_user_id:231fa74d",
    "owner_lark_user_ids": ["ou_owner"],
    "linked_jira_issue": "TCOCP-426",
    "status": "open"
  }
]`

	got, err := parseExtractedPostMortemActions(raw)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("expected 1 row-level action, got %d: %#v", len(got), got)
	}
	if got[0].Title != "故障响应流程优化" {
		t.Fatalf("unexpected title: %s", got[0].Title)
	}
	if !strings.Contains(got[0].Description, "War room 相关机制配套功能") || !strings.Contains(got[0].Description, "订阅AWS,GCP alerts") {
		t.Fatalf("expected full row description to be preserved, got %s", got[0].Description)
	}
	if got[0].LinkedJiraIssue != "TCOCP-426" {
		t.Fatalf("expected row-level Jira link to stay on one action, got %s", got[0].LinkedJiraIssue)
	}
}

func TestExtractActionItemsSection(t *testing.T) {
	content := `# Review
## Summary
ignore
### Action Items
Priority
Item
P1
Fix it
## Appendix
ignore appendix`

	got := extractActionItemsSection(content)
	if strings.Contains(got, "Summary") || strings.Contains(got, "Appendix") {
		t.Fatalf("unexpected section content: %s", got)
	}
	if !strings.Contains(got, "Action Items") || !strings.Contains(got, "Fix it") {
		t.Fatalf("missing action items content: %s", got)
	}
}

func TestExtractActionItemsSectionFallsBackToActionList(t *testing.T) {
	content := `# Review
## Summary
ignore
### Action List
Priority
Item
P1
Fix it
## Appendix
ignore appendix`

	got := extractActionItemsSection(content)
	if strings.Contains(got, "Summary") || strings.Contains(got, "Appendix") {
		t.Fatalf("unexpected section content: %s", got)
	}
	if !strings.Contains(got, "Action List") || !strings.Contains(got, "Fix it") {
		t.Fatalf("missing action list content: %s", got)
	}
}

func TestExtractActionItemsSectionPrefersActionList(t *testing.T) {
	content := `# Review
## Action Items
loose followup list
## Action List
Priority
Item
P1
Structured action
## Appendix
ignore appendix`

	got := extractActionItemsSection(content)
	if strings.Contains(got, "loose followup list") || strings.Contains(got, "Appendix") {
		t.Fatalf("unexpected section content: %s", got)
	}
	if !strings.Contains(got, "Action List") || !strings.Contains(got, "Structured action") {
		t.Fatalf("missing action list content: %s", got)
	}
}

func TestExtractTimelineExtractionContext(t *testing.T) {
	content := `metadata table
Incident Start Time(UTC 0)
2026/05/07 23:54
## Summary
ignore summary
## Timeline
**7:54 utc+8** - alert fired
## Action Items
ignore actions`

	got := extractTimelineExtractionContext(content)
	if !strings.Contains(got, "Incident Start Time(UTC 0)") {
		t.Fatalf("expected incident metadata in timeline context: %s", got)
	}
	if !strings.Contains(got, "**7:54 utc+8** - alert fired") {
		t.Fatalf("expected timeline section in context: %s", got)
	}
	if strings.Contains(got, "ignore summary") || strings.Contains(got, "ignore actions") {
		t.Fatalf("unexpected non-timeline content: %s", got)
	}
}

func TestParseExtractedPostMortemTimelineLogs(t *testing.T) {
	raw := "```json\n[{\"event_time\":\"2026-05-08T07:54:00+08:00\",\"description\":\"客户侧 Slack 首次收到集群告警\"}]\n```"

	logs, err := parseExtractedPostMortemTimelineLogs(raw)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(logs) != 1 {
		t.Fatalf("expected 1 log, got %d", len(logs))
	}
	if logs[0].ItemType != "general" {
		t.Fatalf("unexpected item type: %s", logs[0].ItemType)
	}
	if logs[0].From != "postmortem" {
		t.Fatalf("unexpected from: %s", logs[0].From)
	}
	if logs[0].Description != "客户侧 Slack 首次收到集群告警" {
		t.Fatalf("unexpected description: %s", logs[0].Description)
	}
	if logs[0].EventTime == nil || logs[0].EventTime.Format(time.RFC3339) != "2026-05-08T07:54:00+08:00" {
		t.Fatalf("unexpected event time: %v", logs[0].EventTime)
	}
}

func TestStripLarkUserMentions(t *testing.T) {
	got := stripLarkUserMentions("订阅AWS,GCP alerts @lark_user_id:g77bf2e1")
	if got != "订阅AWS,GCP alerts" {
		t.Fatalf("unexpected cleaned text: %s", got)
	}
}

func TestWithOpsPortalBaseURL(t *testing.T) {
	svc := &incidentService{
		conf: config.IncidentServiceConfig{
			OpsPortalBaseURL: "https://ops-dev.tidbcloud.com/",
		},
	}

	got := svc.withOpsPortalBaseURL("/incidents/30006")
	if got != "https://ops-dev.tidbcloud.com/incidents/30006" {
		t.Fatalf("unexpected incident url: %s", got)
	}

	got = svc.withOpsPortalBaseURL("https://ops.tidbcloud.com/incidents/30006")
	if got != "https://ops.tidbcloud.com/incidents/30006" {
		t.Fatalf("expected absolute url to be preserved, got: %s", got)
	}
}

func TestOpsPortalBaseURLFromEnvironment(t *testing.T) {
	t.Setenv("APP_ENV", "staging")

	svc := &incidentService{}
	got := svc.withOpsPortalBaseURL("/incidents/30006")
	if got != "https://ops-staging.tidbcloud.com/incidents/30006" {
		t.Fatalf("unexpected staging incident url: %s", got)
	}
}
