package service

import (
	"testing"

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

// baseLayer is a weekly a/b/c rotation anchored at monday00 with a stable ID,
// the starting point for the anchor-derivation tests.
func baseLayer() models.TeamLayer {
	return models.TeamLayer{
		ID: 7, TeamID: 1, Priority: 1,
		People:             models.StringList{"a@x.com", "b@x.com", "c@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
		StartsAt: monday00(),
	}
}

func mustNormalize(t *testing.T, in LayerInput, existing []models.TeamLayer, now int64) models.TeamLayer {
	t.Helper()
	out, err := normalizeTeamLayers(1, []LayerInput{in}, existing, now)
	if err != nil {
		t.Fatalf("normalize: %v", err)
	}
	if len(out) != 1 {
		t.Fatalf("want 1 layer, got %d", len(out))
	}
	return out[0]
}

func headAt(t *testing.T, l models.TeamLayer, at int64) string {
	t.Helper()
	heads := layerOnCallAt(l, at)
	if len(heads) == 0 {
		t.Fatalf("no head at %d", at)
	}
	return heads[0]
}

// A resave that does not change the people keeps the entire future schedule:
// same handover instants, same person in every slot.
func TestAnchorNoOpResaveKeepsSchedule(t *testing.T) {
	old := baseLayer()
	now := monday00() + 3*week + 3*24*3600 // Thursday of week 3, head = a
	in := LayerInput{
		ID: old.ID, Priority: 1,
		People:             []string{"a@x.com", "b@x.com", "c@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
	}
	got := mustNormalize(t, in, []models.TeamLayer{old}, now)

	if nb, ob := nextBoundaryAfter(got, now), nextBoundaryAfter(old, now); nb != ob {
		t.Fatalf("handover moved: %d != %d", nb, ob)
	}
	for w := int64(4); w < 10; w++ {
		at := monday00() + w*week + 3600
		if headAt(t, got, at) != headAt(t, old, at) {
			t.Fatalf("week %d head changed: %s != %s", w, headAt(t, got, at), headAt(t, old, at))
		}
	}
}

// next_up_email pins who takes the slot beginning at the inherited handover.
func TestAnchorNextUpPinsPerson(t *testing.T) {
	old := baseLayer()
	now := monday00() + 3*week + 3*24*3600
	in := LayerInput{
		ID: old.ID, Priority: 1,
		People:             []string{"a@x.com", "b@x.com", "c@x.com", "d@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
		NextUpEmail: "d@x.com",
	}
	got := mustNormalize(t, in, []models.TeamLayer{old}, now)

	handoff := nextBoundaryAfter(old, now) // monday00 + 4 weeks
	if nb := nextBoundaryAfter(got, now); nb != handoff {
		t.Fatalf("handover moved: %d != %d", nb, handoff)
	}
	if h := headAt(t, got, handoff+3600); h != "d@x.com" {
		t.Fatalf("head at handoff = %s, want d@x.com", h)
	}
	// Rotation continues in list order after the pinned slot (wraps to a).
	if h := headAt(t, got, handoff+week+3600); h != "a@x.com" {
		t.Fatalf("head after pinned slot = %s, want a@x.com", h)
	}
}

// Without next_up_email, inserting someone defaults the next slot to the
// current head's successor in the NEW order — the inserted person, if they
// were placed right after the person on duty.
func TestAnchorDefaultSuccessorAfterInsert(t *testing.T) {
	old := baseLayer()
	now := monday00() + 3*week + 3*24*3600 // head = a
	in := LayerInput{
		ID: old.ID, Priority: 1,
		People:             []string{"a@x.com", "d@x.com", "b@x.com", "c@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
	}
	got := mustNormalize(t, in, []models.TeamLayer{old}, now)

	handoff := nextBoundaryAfter(old, now)
	if h := headAt(t, got, handoff+3600); h != "d@x.com" {
		t.Fatalf("head at handoff = %s, want d@x.com", h)
	}
}

// Removing the person on duty falls back to whoever was next under the old
// rules; the handover instant itself is preserved.
func TestAnchorRemoveCurrentHead(t *testing.T) {
	old := baseLayer()
	now := monday00() + 3*week + 3*24*3600 // head = a
	in := LayerInput{
		ID: old.ID, Priority: 1,
		People:             []string{"b@x.com", "c@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
	}
	got := mustNormalize(t, in, []models.TeamLayer{old}, now)

	handoff := nextBoundaryAfter(old, now)
	if nb := nextBoundaryAfter(got, now); nb != handoff {
		t.Fatalf("handover moved: %d != %d", nb, handoff)
	}
	if h := headAt(t, got, handoff+3600); h != "b@x.com" {
		t.Fatalf("head at handoff = %s, want b@x.com", h)
	}
	if h := headAt(t, got, handoff+week+3600); h != "c@x.com" {
		t.Fatalf("head after handoff = %s, want c@x.com", h)
	}
}

// An explicit handoff_at re-anchors the handover grid.
func TestAnchorExplicitHandoff(t *testing.T) {
	old := baseLayer()
	now := monday00() + 3*week + 3*24*3600
	newHandoff := monday00() + 4*week + 2*24*3600 // shift handovers to Wednesday
	in := LayerInput{
		ID: old.ID, Priority: 1,
		People:             []string{"a@x.com", "b@x.com", "c@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
		HandoffAt: newHandoff, NextUpEmail: "b@x.com",
	}
	got := mustNormalize(t, in, []models.TeamLayer{old}, now)

	if nb := nextBoundaryAfter(got, now); nb != newHandoff {
		t.Fatalf("handover = %d, want %d", nb, newHandoff)
	}
	if h := headAt(t, got, newHandoff+3600); h != "b@x.com" {
		t.Fatalf("head at new handoff = %s, want b@x.com", h)
	}
}

func TestNormalizeRejectsBadNextUpAndUnknownID(t *testing.T) {
	old := baseLayer()
	now := monday00() + week
	if _, err := normalizeTeamLayers(1, []LayerInput{{
		ID: old.ID, Priority: 1, People: []string{"a@x.com", "b@x.com"},
		RotateEverySeconds: week, NextUpEmail: "ghost@x.com",
	}}, []models.TeamLayer{old}, now); err == nil {
		t.Fatal("want error for next_up_email not in people")
	}
	if _, err := normalizeTeamLayers(1, []LayerInput{{
		ID: 999, Priority: 1, People: []string{"a@x.com"},
	}}, []models.TeamLayer{old}, now); err == nil {
		t.Fatal("want error for unknown layer id")
	}
}

// ReplaceTeamLayers updates rows in place when ids are passed (the layer keeps
// its identity across saves) and PreviewTeamLayers never writes.
func TestReplaceInPlaceAndPreviewDryRun(t *testing.T) {
	newTestDB(t)
	cat := models.Team{Name: "t", DisplayName: "t"}
	db.DB.Create(&cat)
	aID := mkEngineer(t, "Alice", "a@x.com")
	bID := mkEngineer(t, "Bob", "b@x.com")
	db.DB.Create(&models.EngineerTeam{EngineerID: aID, TeamID: cat.ID})
	db.DB.Create(&models.EngineerTeam{EngineerID: bID, TeamID: cat.ID})

	s := &ScheduleService{}
	if _, err := s.ReplaceTeamLayers(cat.ID, []LayerInput{{
		Priority: 1, People: []string{"a@x.com", "b@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
	}}); err != nil {
		t.Fatalf("create: %v", err)
	}
	var created models.TeamLayer
	if err := db.DB.Where("team_id = ?", cat.ID).First(&created).Error; err != nil {
		t.Fatal(err)
	}

	// Preview a change referencing a non-member: warning surfaces, no write.
	pv, err := s.PreviewTeamLayers(cat.ID, []LayerInput{{
		ID: created.ID, Priority: 1, People: []string{"a@x.com", "b@x.com", "x@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
	}})
	if err != nil {
		t.Fatalf("preview: %v", err)
	}
	warnings, _ := pv["warnings"].([]string)
	if len(warnings) != 1 || warnings[0] != "x@x.com is not a member of this team" {
		t.Fatalf("warnings = %v", warnings)
	}
	if len(pv["segments"].([]ComputedSegment)) == 0 {
		t.Fatal("preview returned no segments")
	}
	var after models.TeamLayer
	db.DB.Where("team_id = ?", cat.ID).First(&after)
	if len(after.People) != 2 {
		t.Fatalf("preview wrote to the db: %v", after.People)
	}

	// Save with the id: same row updated, no new rows.
	if _, err := s.ReplaceTeamLayers(cat.ID, []LayerInput{{
		ID: created.ID, Priority: 1, People: []string{"b@x.com", "a@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
	}}); err != nil {
		t.Fatalf("update: %v", err)
	}
	var rows []models.TeamLayer
	db.DB.Where("team_id = ?", cat.ID).Find(&rows)
	if len(rows) != 1 || rows[0].ID != created.ID {
		t.Fatalf("want the same single layer, got %+v", rows)
	}
	if rows[0].People[0] != "b@x.com" {
		t.Fatalf("people not updated: %v", rows[0].People)
	}

	// Omitting the id drops the old layer and creates a fresh one.
	if _, err := s.ReplaceTeamLayers(cat.ID, []LayerInput{{
		Priority: 1, People: []string{"a@x.com"},
		RotateEverySeconds: week, OnCallAtOnce: 1, ActiveMode: "always",
	}}); err != nil {
		t.Fatalf("recreate: %v", err)
	}
	db.DB.Where("team_id = ?", cat.ID).Find(&rows)
	if len(rows) != 1 || rows[0].ID == created.ID {
		t.Fatalf("want a fresh layer, got %+v", rows)
	}
}
