package service

import (
	"fmt"
	"strings"
	"testing"

	"github.com/tidbcloud/aisupport/services/oncall/db"
	"github.com/tidbcloud/aisupport/services/oncall/models"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func setupNotifyTestDB(t *testing.T) {
	t.Helper()

	var err error
	db.DB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		t.Fatalf("failed to connect sqlite: %v", err)
	}

	err = db.DB.AutoMigrate(&models.Team{}, &models.Engineer{}, &models.ShiftInterval{},
		&models.TeamLayer{}, &models.RealizedShift{})
	if err != nil {
		t.Fatalf("failed to migrate: %v", err)
	}
}

func TestBuildFeishuNotifyPreview_ListsAllAndPrevious(t *testing.T) {
	setupNotifyTestDB(t)

	catA := models.Team{Name: "cat_a", DisplayName: "Team A", GroupNames: models.StringList{"ONCALL"}}
	catB := models.Team{Name: "cat_b", DisplayName: "Team B", GroupNames: models.StringList{"ONCALL"}}
	db.DB.Create(&catA)
	db.DB.Create(&catB)

	for _, eng := range []*models.Engineer{
		{Name: "PrevA", Email: "preva@pingcap.com"},
		{Name: "CurrA", Email: "curra@pingcap.com"},
		{Name: "Bx", Email: "bx@pingcap.com"},
		{Name: "By", Email: "by@pingcap.com"},
		{Name: "Bz", Email: "bz@pingcap.com"},
	} {
		if err := db.DB.Create(eng).Error; err != nil {
			t.Fatalf("create engineer: %v", err)
		}
	}

	// Team A: weekly rotation preva -> curra.
	db.DB.Create(&models.TeamLayer{
		TeamID: catA.ID, Priority: 1,
		People:             models.StringList{"preva@pingcap.com", "curra@pingcap.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always", StartsAt: monday00(),
	})
	// Team B: two concurrent on call, weekly.
	db.DB.Create(&models.TeamLayer{
		TeamID: catB.ID, Priority: 1,
		People:             models.StringList{"bx@pingcap.com", "by@pingcap.com", "bz@pingcap.com"},
		RotateEverySeconds: week, OnCallAtOnce: 2, ActiveMode: "always", StartsAt: monday00(),
	})

	s := &ScheduleService{}
	// Mid week 1: A's current = CurrA, previous (week 0) = PrevA.
	preview, err := s.BuildFeishuNotifyPreview("ONCALL", monday00()+week+3600, false, false)
	if err != nil {
		t.Fatalf("BuildFeishuNotifyPreview err: %v", err)
	}
	if len(preview.Items) != 2 {
		t.Fatalf("expected 2 preview items, got %d", len(preview.Items))
	}
	if !strings.Contains(preview.MessageText, "Team A: @CurrA substitutes @PrevA") {
		t.Fatalf("A rotation handover wrong: %s", preview.MessageText)
	}
	// B lists both concurrent on-call. The group advances by 2 each week, so with
	// 3 people week1 = bz,bx (bx wraps in as the second slot; names shown sorted).
	if !strings.Contains(preview.MessageText, "Team B: @Bx, Bz substitutes @Bx, By") {
		t.Fatalf("B should list both concurrent on-call (week1 = bx,bz): %s", preview.MessageText)
	}
}

func TestBuildWeeklyRosterCard_ContainsExpectedContentAndMentions(t *testing.T) {
	setupNotifyTestDB(t)
	if err := db.DB.AutoMigrate(&models.NotificationLog{}); err != nil {
		t.Fatalf("migrate: %v", err)
	}
	withWeeklyRosterMentionResolver(t, func(emails []string) (map[string]string, error) {
		return map[string]string{"a@pingcap.com": "ou_alice"}, nil
	})
	const day = int64(24 * 3600)

	mkEngineers(t, map[string]string{"a@pingcap.com": "Alice", "b@pingcap.com": "Bob"})
	rot := models.Team{Name: "rot", DisplayName: "Team Rot", GroupNames: models.StringList{"ONCALL"}, SortPriority: 1}
	other := models.Team{Name: "other", DisplayName: "Team Other", GroupNames: models.StringList{"PLATFORM"}, SortPriority: 2}
	for _, c := range []*models.Team{&rot, &other} {
		db.DB.Create(c)
	}
	db.DB.Create(&models.TeamLayer{
		TeamID: rot.ID, Priority: 1, People: models.StringList{"a@pingcap.com", "b@pingcap.com"},
		RotateEverySeconds: day, OnCallAtOnce: 1, ActiveMode: "always", StartsAt: monday00(),
	})
	db.DB.Create(&models.TeamLayer{
		TeamID: other.ID, Priority: 1, People: models.StringList{"b@pingcap.com"},
		RotateEverySeconds: day, OnCallAtOnce: 1, ActiveMode: "always", StartsAt: monday00(),
	})

	s := &ScheduleService{}
	card, hash, err := s.BuildWeeklyRosterCard(monday00(), monday00()+2*day)
	if err != nil {
		t.Fatalf("BuildWeeklyRosterCard: %v", err)
	}
	if hash == "" {
		t.Fatalf("expected non-empty content hash")
	}

	cfg, _ := card["config"].(map[string]interface{})
	if cfg["update_multi"] != true {
		t.Fatalf("card config must set update_multi:true for later patching, got %v", cfg)
	}

	rendered := fmt.Sprintf("%v", card)
	checks := []string{
		"On-Call Schedule",
		"Full schedule → https://oncall.pingcap.net",
		`Team Rot`,
		`<at id="ou_alice"></at>`,
		"Need a change? Please comment in this topic AND update it in the on-call system.",
	}
	for _, c := range checks {
		if !strings.Contains(rendered, c) {
			t.Fatalf("card missing %q\n---\n%s", c, rendered)
		}
	}
	if strings.Contains(rendered, "Team Other") {
		t.Fatalf("non-ONCALL group team should be excluded:\n%s", rendered)
	}
	// Bob has no resolvable Feishu ID in this test's resolver, so falls back to plain text.
	if !strings.Contains(rendered, "Bob") || strings.Contains(rendered, `<at id="ou_bob">`) {
		t.Fatalf("Bob should appear as plain text fallback:\n%s", rendered)
	}
}

func TestBuildWeeklyRosterCard_HashChangesWithContent(t *testing.T) {
	setupNotifyTestDB(t)
	if err := db.DB.AutoMigrate(&models.NotificationLog{}); err != nil {
		t.Fatalf("migrate: %v", err)
	}
	withWeeklyRosterMentionResolver(t, func(emails []string) (map[string]string, error) {
		return map[string]string{}, nil
	})
	const day = int64(24 * 3600)

	mkEngineers(t, map[string]string{"a@pingcap.com": "Alice", "b@pingcap.com": "Bob"})
	team := models.Team{Name: "rot", DisplayName: "Team Rot", GroupNames: models.StringList{"ONCALL"}, SortPriority: 1}
	if err := db.DB.Create(&team).Error; err != nil {
		t.Fatalf("create team: %v", err)
	}
	db.DB.Create(&models.TeamLayer{
		TeamID: team.ID, Priority: 1, People: models.StringList{"a@pingcap.com"},
		RotateEverySeconds: 100 * day, OnCallAtOnce: 1, ActiveMode: "always", StartsAt: monday00(),
	})

	s := &ScheduleService{}
	_, hash1, err := s.BuildWeeklyRosterCard(monday00(), monday00()+day)
	if err != nil {
		t.Fatalf("BuildWeeklyRosterCard: %v", err)
	}
	_, hash1Again, err := s.BuildWeeklyRosterCard(monday00(), monday00()+day)
	if err != nil {
		t.Fatalf("BuildWeeklyRosterCard: %v", err)
	}
	if hash1 != hash1Again {
		t.Fatalf("hash should be stable for identical input: %q vs %q", hash1, hash1Again)
	}

	db.DB.Model(&models.TeamLayer{}).Where("team_id = ?", team.ID).
		Update("people", models.StringList{"b@pingcap.com"})

	_, hash2, err := s.BuildWeeklyRosterCard(monday00(), monday00()+day)
	if err != nil {
		t.Fatalf("BuildWeeklyRosterCard: %v", err)
	}
	if hash1 == hash2 {
		t.Fatalf("hash should change when roster content changes")
	}
}

func TestBuildWeeklyRosterLiveCard_SplitsCurrentAndOthers(t *testing.T) {
	setupNotifyTestDB(t)
	if err := db.DB.AutoMigrate(&models.NotificationLog{}); err != nil {
		t.Fatalf("migrate: %v", err)
	}
	withWeeklyRosterMentionResolver(t, func(emails []string) (map[string]string, error) {
		return map[string]string{"a@pingcap.com": "ou_alice"}, nil
	})
	const day = int64(24 * 3600)

	mkEngineers(t, map[string]string{"a@pingcap.com": "Alice", "b@pingcap.com": "Bob", "c@pingcap.com": "Carol"})
	team := models.Team{Name: "rot", DisplayName: "Team Rot", GroupNames: models.StringList{"ONCALL"}, SortPriority: 1}
	if err := db.DB.Create(&team).Error; err != nil {
		t.Fatalf("create team: %v", err)
	}
	// Daily rotation: Alice day0, Bob day1, Carol day2.
	if err := db.DB.Create(&models.TeamLayer{
		TeamID: team.ID, Priority: 1, People: models.StringList{"a@pingcap.com", "b@pingcap.com", "c@pingcap.com"},
		RotateEverySeconds: day, OnCallAtOnce: 1, ActiveMode: "always", StartsAt: monday00(),
	}).Error; err != nil {
		t.Fatalf("create layer: %v", err)
	}

	s := &ScheduleService{}
	from := monday00()
	to := monday00() + 3*day
	now := monday00() + day + 3600 // an hour into Bob's (day1) shift

	card, hash, err := s.BuildWeeklyRosterLiveCard(from, to, now)
	if err != nil {
		t.Fatalf("BuildWeeklyRosterLiveCard: %v", err)
	}
	if hash == "" {
		t.Fatalf("expected non-empty content hash")
	}

	cfg, _ := card["config"].(map[string]interface{})
	if cfg["update_multi"] != true {
		t.Fatalf("card config must set update_multi:true, got %v", cfg)
	}

	rendered := fmt.Sprintf("%v", card)
	if !strings.Contains(rendered, "Full schedule → https://oncall.pingcap.net") {
		t.Fatalf("card should link to the schedule: %s", rendered)
	}
	if !strings.Contains(rendered, "Bob") {
		t.Fatalf("card should show Bob as current: %s", rendered)
	}
	if !strings.Contains(rendered, "also on call this week but not now") {
		t.Fatalf("card should list the rest of the week's roster as not-currently-on-call: %s", rendered)
	}
	// Alice has a resolvable Feishu ID; since she's in the "others" bucket (not
	// current), she must NOT be rendered as an <at> mention there.
	if strings.Contains(rendered, `<at id="ou_alice">`) {
		t.Fatalf("others should not be @ mentioned even if resolvable: %s", rendered)
	}
	if !strings.Contains(rendered, "Alice") || !strings.Contains(rendered, "Carol") {
		t.Fatalf("both other roster members should be named in the footnote: %s", rendered)
	}
}
