package service

import (
	"testing"

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

func emailsFromRows(rows []map[string]interface{}) map[string]bool {
	out := map[string]bool{}
	for _, r := range rows {
		if e, _ := r["engineer_email"].(string); e != "" {
			out[e] = true
		}
	}
	return out
}

// Req1: an append layer unions its people with the lower override layer.
func TestAppendLayerUnions(t *testing.T) {
	newTestDB(t)
	s := &ScheduleService{}
	team := models.Team{Name: "t", DisplayName: "t"}
	db.DB.Create(&team)
	db.DB.Create(&models.Engineer{Name: "A", Email: "a@x.com"})
	db.DB.Create(&models.Engineer{Name: "B", Email: "b@x.com"})
	// base (override) + a higher-priority append layer, both always active.
	db.DB.Create(&models.TeamLayer{TeamID: team.ID, Priority: 1, People: models.StringList{"a@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always", CombineMode: "override"})
	db.DB.Create(&models.TeamLayer{TeamID: team.ID, Priority: 2, People: models.StringList{"b@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always", CombineMode: "append"})

	res, err := s.GetCurrentOnCall(team.ID, 0)
	if err != nil {
		t.Fatal(err)
	}
	got := map[string]bool{}
	for _, e := range res.CurrentEngineerEmails {
		got[e] = true
	}
	if !got["a@x.com"] || !got["b@x.com"] {
		t.Fatalf("append should union both layers, got %v", res.CurrentEngineerEmails)
	}
}

// Req2: support covers an RD team inside a window, falls back to RD outside it.
func TestReferenceCoverWithFallback(t *testing.T) {
	newTestDB(t)
	s := &ScheduleService{}
	support := models.Team{Name: "support", DisplayName: "S"}
	rd := models.Team{Name: "rd", DisplayName: "R"}
	db.DB.Create(&support)
	db.DB.Create(&rd)
	db.DB.Create(&models.Engineer{Name: "Sup", Email: "sup@x.com"})
	db.DB.Create(&models.Engineer{Name: "Rd", Email: "rd@x.com"})
	db.DB.Create(&models.TeamLayer{TeamID: support.ID, Priority: 1, People: models.StringList{"sup@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always"})
	db.DB.Create(&models.TeamLayer{TeamID: rd.ID, Priority: 1, People: models.StringList{"rd@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always"})

	// Composite: support covers 00:00–12:00 (UTC+8), else fall back to rd.
	cover := models.Team{Name: "rd-oncall", DisplayName: "RD On-call"}
	db.DB.Create(&cover)
	db.DB.Create(&models.TeamLayer{TeamID: cover.ID, Priority: 100, SourceTeamID: support.ID,
		CombineMode: "override", ActiveMode: "window", ActiveDays: models.IntList{0, 1, 2, 3, 4, 5, 6},
		ActiveStartMin: 0, ActiveEndMin: 720})
	db.DB.Create(&models.TeamLayer{TeamID: cover.ID, Priority: 0, SourceTeamID: rd.ID,
		CombineMode: "override", ActiveMode: "always"})

	inside := monday00() + 6*3600   // 06:00 → inside support window
	outside := monday00() + 18*3600 // 18:00 → outside

	in, err := s.GetEffectiveScheduleV2([]uint{cover.ID}, inside, inside+1, false, false)
	if err != nil {
		t.Fatal(err)
	}
	if g := emailsFromRows(in); !g["sup@x.com"] || g["rd@x.com"] {
		t.Fatalf("inside window should be support only, got %v", g)
	}
	out, err := s.GetEffectiveScheduleV2([]uint{cover.ID}, outside, outside+1, false, false)
	if err != nil {
		t.Fatal(err)
	}
	if g := emailsFromRows(out); !g["rd@x.com"] || g["sup@x.com"] {
		t.Fatalf("outside window should fall back to rd, got %v", g)
	}
}

// A team may now mix people layers and reference layers: RD's own rotation as
// the base, with support overriding during its window.
func TestMixedPeopleAndReferenceLayers(t *testing.T) {
	newTestDB(t)
	s := &ScheduleService{}
	support := models.Team{Name: "support", DisplayName: "S"}
	db.DB.Create(&support)
	db.DB.Create(&models.Engineer{Name: "Sup", Email: "sup@x.com"})
	db.DB.Create(&models.Engineer{Name: "Rd", Email: "rd@x.com"})
	db.DB.Create(&models.TeamLayer{TeamID: support.ID, Priority: 1, People: models.StringList{"sup@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always"})

	// rd team: own people layer (always) + support reference layer (window, override).
	rd := models.Team{Name: "rd", DisplayName: "R"}
	db.DB.Create(&rd)
	db.DB.Create(&models.TeamLayer{TeamID: rd.ID, Priority: 0, People: models.StringList{"rd@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always", CombineMode: "override"})
	db.DB.Create(&models.TeamLayer{TeamID: rd.ID, Priority: 100, SourceTeamID: support.ID,
		CombineMode: "override", ActiveMode: "window", ActiveDays: models.IntList{0, 1, 2, 3, 4, 5, 6},
		ActiveStartMin: 0, ActiveEndMin: 720})

	inside := monday00() + 6*3600
	outside := monday00() + 18*3600

	in, err := s.GetEffectiveScheduleV2([]uint{rd.ID}, inside, inside+1, false, false)
	if err != nil {
		t.Fatal(err)
	}
	if g := emailsFromRows(in); !g["sup@x.com"] || g["rd@x.com"] {
		t.Fatalf("inside window support should cover, got %v", g)
	}
	out, err := s.GetEffectiveScheduleV2([]uint{rd.ID}, outside, outside+1, false, false)
	if err != nil {
		t.Fatal(err)
	}
	if g := emailsFromRows(out); !g["rd@x.com"] || g["sup@x.com"] {
		t.Fatalf("outside window rd's own rotation should hold, got %v", g)
	}

	// A mixed team can be frozen (resolve-through) without error.
	if _, err := s.ReplaceTeamLayers(rd.ID, []LayerInput{
		{People: []string{"rd@x.com"}, ActiveMode: "always"},
		{SourceTeamID: support.ID, ActiveMode: "window", ActiveDays: []int{0, 1, 2, 3, 4, 5, 6}, ActiveStartMin: 0, ActiveEndMin: 720},
	}); err != nil {
		t.Fatalf("mixed layers should be allowed and freeze cleanly: %v", err)
	}
}

// A baseline person without an engineer record must still appear in the
// effective schedule (it did in the computed preview), so Schedule and Rotation
// preview agree — an append layer's people don't silently vanish.
func TestEffectiveShowsUnregisteredBaseline(t *testing.T) {
	newTestDB(t)
	s := &ScheduleService{}
	team := models.Team{Name: "t", DisplayName: "t"}
	db.DB.Create(&team)
	db.DB.Create(&models.Engineer{Name: "A", Email: "a@x.com"})
	// b@x.com has no engineer record on purpose.
	db.DB.Create(&models.TeamLayer{TeamID: team.ID, Priority: 1, People: models.StringList{"a@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always", CombineMode: "override"})
	db.DB.Create(&models.TeamLayer{TeamID: team.ID, Priority: 2, People: models.StringList{"b@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always", CombineMode: "append"})

	now := monday00()
	rows, err := s.GetEffectiveScheduleV2([]uint{team.ID}, now, now+3600, false, false)
	if err != nil {
		t.Fatal(err)
	}
	g := emailsFromRows(rows)
	if !g["a@x.com"] || !g["b@x.com"] {
		t.Fatalf("effective should include both append people, got %v", g)
	}
}

// Reference targets are still validated: no self-reference, no nesting.
func TestReferenceValidation(t *testing.T) {
	newTestDB(t)
	s := &ScheduleService{}
	support := models.Team{Name: "support", DisplayName: "S"}
	rd := models.Team{Name: "rd", DisplayName: "R"}
	cover := models.Team{Name: "cover", DisplayName: "C"}
	db.DB.Create(&support)
	db.DB.Create(&rd)
	db.DB.Create(&cover)
	db.DB.Create(&models.Engineer{Name: "Sup", Email: "sup@x.com"})
	db.DB.Create(&models.TeamLayer{TeamID: support.ID, Priority: 1, People: models.StringList{"sup@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always"})
	// cover references support → cover is itself a reference-bearing team.
	db.DB.Create(&models.TeamLayer{TeamID: cover.ID, Priority: 1, SourceTeamID: support.ID,
		CombineMode: "override", ActiveMode: "always"})

	if _, err := s.ReplaceTeamLayers(rd.ID, []LayerInput{
		{SourceTeamID: rd.ID, ActiveMode: "always"},
	}); err == nil {
		t.Fatal("expected self-reference to be rejected")
	}
	if _, err := s.ReplaceTeamLayers(rd.ID, []LayerInput{
		{SourceTeamID: cover.ID, ActiveMode: "always"},
	}); err == nil {
		t.Fatal("expected reference to a reference-bearing team (nesting) to be rejected")
	}
}

// Batched overview path: several teams in one GetEffectiveScheduleV2 call that
// all reference the SAME source team must each resolve correctly. This locks in
// batchRefTimelines (one nested resolution shared across teams) as equivalent to
// the old per-team refTimelinesFor fan-out, and checks rows stay keyed by team.
func TestBatchedReferenceResolutionAcrossTeams(t *testing.T) {
	newTestDB(t)
	s := &ScheduleService{}
	support := models.Team{Name: "support", DisplayName: "S"}
	db.DB.Create(&support)
	db.DB.Create(&models.Engineer{Name: "Sup", Email: "sup@x.com"})
	db.DB.Create(&models.TeamLayer{TeamID: support.ID, Priority: 1, People: models.StringList{"sup@x.com"},
		OnCallAtOnce: 1, ActiveMode: "always"})

	// Two independent teams that both reference the single support team.
	coverA := models.Team{Name: "cover_a", DisplayName: "A"}
	coverB := models.Team{Name: "cover_b", DisplayName: "B"}
	db.DB.Create(&coverA)
	db.DB.Create(&coverB)
	for _, id := range []uint{coverA.ID, coverB.ID} {
		db.DB.Create(&models.TeamLayer{TeamID: id, Priority: 1, SourceTeamID: support.ID,
			CombineMode: "override", ActiveMode: "always"})
	}

	now := monday00() + 6*3600
	rows, err := s.GetEffectiveScheduleV2([]uint{coverA.ID, coverB.ID}, now, now+1, false, false)
	if err != nil {
		t.Fatal(err)
	}
	// Each team must independently resolve to support's on-call.
	byTeam := map[uint]map[string]bool{}
	for _, r := range rows {
		tid, _ := r["team_id"].(uint)
		em, _ := r["engineer_email"].(string)
		if byTeam[tid] == nil {
			byTeam[tid] = map[string]bool{}
		}
		if em != "" {
			byTeam[tid][em] = true
		}
	}
	for _, id := range []uint{coverA.ID, coverB.ID} {
		if !byTeam[id]["sup@x.com"] {
			t.Fatalf("team %d should resolve to sup@x.com via batched reference, got %v", id, byTeam[id])
		}
	}
}
