package service

import (
	"errors"
	"fmt"
	"strings"
	"testing"

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

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

// setupRoleTestDB gives each test its own named in-memory database. It has to
// be shared-cache rather than plain ":memory:" so the connections a
// transaction spans see the same data, and named so tests don't collide.
func setupRoleTestDB(t *testing.T) {
	t.Helper()

	dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared",
		strings.NewReplacer("/", "_", " ", "_").Replace(t.Name()))
	var err error
	db.DB, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	if err != nil {
		t.Fatalf("failed to connect sqlite: %v", err)
	}
	if err := db.DB.AutoMigrate(&models.Team{}, &models.Engineer{}, &models.EngineerTeam{}, &models.SuperAdmin{}); err != nil {
		t.Fatalf("failed to migrate: %v", err)
	}

	// Permission checks short-circuit when auth is off, which would make these
	// assertions vacuous.
	t.Setenv("AUTH0_ENABLED", "true")
	t.Setenv("DEV_BYPASS_AUTH", "false")
	t.Setenv("ALLOWED_EMAIL_DOMAINS", "pingcap.com")
	t.Setenv("FEISHU_APP_ID", "")
	t.Setenv("FEISHU_APP_SECRET", "")
}

// seedTeamWithRoles creates a team plus one engineer per (email, role) pair.
func seedTeamWithRoles(t *testing.T, teamName string, roles map[string]string) models.Team {
	t.Helper()

	team := models.Team{Name: teamName, DisplayName: teamName}
	if err := db.DB.Create(&team).Error; err != nil {
		t.Fatalf("create team: %v", err)
	}
	for email, role := range roles {
		eng := models.Engineer{Name: email, Email: email}
		if err := db.DB.Create(&eng).Error; err != nil {
			t.Fatalf("create engineer %s: %v", email, err)
		}
		if err := db.DB.Create(&models.EngineerTeam{EngineerID: eng.ID, TeamID: team.ID, Role: role}).Error; err != nil {
			t.Fatalf("create link %s: %v", email, err)
		}
	}
	return team
}

func engineerIDByEmail(t *testing.T, email string) uint {
	t.Helper()
	var eng models.Engineer
	if err := db.DB.Where("email = ?", email).First(&eng).Error; err != nil {
		t.Fatalf("find engineer %s: %v", email, err)
	}
	return eng.ID
}

// TestTeamRoleLadder pins the whole point of the editor role: an editor may
// write the schedule but may not hand out roles or disable the team.
func TestTeamRoleLadder(t *testing.T) {
	setupRoleTestDB(t)

	team := seedTeamWithRoles(t, "team_ladder", map[string]string{
		"manager@pingcap.com": models.TeamRoleManager,
		"editor@pingcap.com":  models.TeamRoleEditor,
		"member@pingcap.com":  models.TeamRoleMember,
	})

	s := &ScheduleService{}
	cases := []struct {
		email        string
		wantWrite    bool
		wantAdmin    bool
		wantRoleName string
	}{
		{"manager@pingcap.com", true, true, models.TeamRoleManager},
		{"editor@pingcap.com", true, false, models.TeamRoleEditor},
		{"member@pingcap.com", false, false, models.TeamRoleMember},
		{"outsider@pingcap.com", false, false, ""},
	}
	for _, tc := range cases {
		role, err := s.TeamRoleOf(team.ID, tc.email)
		if err != nil {
			t.Fatalf("%s: TeamRoleOf: %v", tc.email, err)
		}
		if role != tc.wantRoleName {
			t.Fatalf("%s: role = %q, want %q", tc.email, role, tc.wantRoleName)
		}
		if got := s.EnsureTeamWritePermission(team.ID, tc.email) == nil; got != tc.wantWrite {
			t.Fatalf("%s: write permission = %v, want %v", tc.email, got, tc.wantWrite)
		}
		if got := s.EnsureTeamAdminPermission(team.ID, tc.email) == nil; got != tc.wantAdmin {
			t.Fatalf("%s: admin permission = %v, want %v", tc.email, got, tc.wantAdmin)
		}
	}
}

// TestEnsureCanManageMember closes the demotion backdoor: removing a member is
// also a way to strip their role, so an editor may only remove plain members.
func TestEnsureCanManageMember(t *testing.T) {
	setupRoleTestDB(t)

	team := seedTeamWithRoles(t, "team_manage", map[string]string{
		"manager@pingcap.com":  models.TeamRoleManager,
		"editor@pingcap.com":   models.TeamRoleEditor,
		"editor2@pingcap.com":  models.TeamRoleEditor,
		"member@pingcap.com":   models.TeamRoleMember,
		"outsider@pingcap.com": models.TeamRoleMember,
	})

	s := &ScheduleService{}
	cases := []struct {
		name   string
		actor  string
		target string
		wantOK bool
	}{
		{"editor removes member", "editor@pingcap.com", "member@pingcap.com", true},
		{"editor removes manager", "editor@pingcap.com", "manager@pingcap.com", false},
		{"editor removes another editor", "editor@pingcap.com", "editor2@pingcap.com", false},
		{"editor removes themselves", "editor@pingcap.com", "editor@pingcap.com", false},
		{"manager removes editor", "manager@pingcap.com", "editor@pingcap.com", true},
		{"manager removes manager", "manager@pingcap.com", "manager@pingcap.com", true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := s.EnsureCanManageMember(team.ID, tc.actor, engineerIDByEmail(t, tc.target))
			if (err == nil) != tc.wantOK {
				t.Fatalf("EnsureCanManageMember = %v, want ok=%v", err, tc.wantOK)
			}
		})
	}
}

// TestDeleteEngineerFromTeam_KeepsLastManager guards the other half of that
// backdoor: the last manager cannot be dropped via the membership endpoint
// either, which previously bypassed the manager-revoke check entirely.
func TestDeleteEngineerFromTeam_KeepsLastManager(t *testing.T) {
	setupRoleTestDB(t)

	team := seedTeamWithRoles(t, "team_last", map[string]string{
		"manager@pingcap.com": models.TeamRoleManager,
		"editor@pingcap.com":  models.TeamRoleEditor,
	})

	s := &ScheduleService{}
	err := s.DeleteEngineerFromTeam(engineerIDByEmail(t, "manager@pingcap.com"), team.ID)
	if !errors.Is(err, ErrLastManager) {
		t.Fatalf("delete last manager = %v, want ErrLastManager", err)
	}

	// The editor is not load-bearing for the team's administration, so removing
	// them is fine.
	if err := s.DeleteEngineerFromTeam(engineerIDByEmail(t, "editor@pingcap.com"), team.ID); err != nil {
		t.Fatalf("delete editor: %v", err)
	}
}

func TestSetTeamMemberRole(t *testing.T) {
	setupRoleTestDB(t)

	team := seedTeamWithRoles(t, "team_setrole", map[string]string{
		"manager@pingcap.com":  models.TeamRoleManager,
		"manager2@pingcap.com": models.TeamRoleManager,
		"member@pingcap.com":   models.TeamRoleMember,
	})
	s := &ScheduleService{}

	memberID := engineerIDByEmail(t, "member@pingcap.com")
	if err := s.SetTeamMemberRole(memberID, team.ID, models.TeamRoleEditor); err != nil {
		t.Fatalf("promote to editor: %v", err)
	}
	role, err := s.TeamRoleOf(team.ID, "member@pingcap.com")
	if err != nil || role != models.TeamRoleEditor {
		t.Fatalf("role after promote = %q (err %v), want editor", role, err)
	}

	// The legacy mirror must track the ladder, or a rollback to a binary that
	// still reads is_manager would see the wrong manager set.
	var link models.EngineerTeam
	if err := db.DB.Where("team_id = ? AND engineer_id = ?", team.ID, memberID).First(&link).Error; err != nil {
		t.Fatalf("load link: %v", err)
	}
	if link.IsManager {
		t.Fatalf("is_manager mirror = true for an editor")
	}
	if err := s.SetTeamMemberRole(memberID, team.ID, models.TeamRoleManager); err != nil {
		t.Fatalf("promote to manager: %v", err)
	}
	if err := db.DB.Where("team_id = ? AND engineer_id = ?", team.ID, memberID).First(&link).Error; err != nil {
		t.Fatalf("reload link: %v", err)
	}
	if !link.IsManager {
		t.Fatalf("is_manager mirror = false for a manager")
	}

	if err := s.SetTeamMemberRole(memberID, team.ID, "admin"); err == nil {
		t.Fatalf("expected an unknown role to be rejected")
	}
}

func TestSetTeamMemberRole_RejectsDemotingLastManager(t *testing.T) {
	setupRoleTestDB(t)

	team := seedTeamWithRoles(t, "team_demote", map[string]string{
		"manager@pingcap.com": models.TeamRoleManager,
	})

	s := &ScheduleService{}
	err := s.SetTeamMemberRole(engineerIDByEmail(t, "manager@pingcap.com"), team.ID, models.TeamRoleEditor)
	if !errors.Is(err, ErrLastManager) {
		t.Fatalf("demote last manager = %v, want ErrLastManager", err)
	}
}

// TestUpdateTeam_ManagersReplacementKeepsEditors covers the subtle case in the
// PATCH path: the managers list replaces the manager set, and must not sweep
// editors down to plain members on its way through.
func TestUpdateTeam_ManagersReplacementKeepsEditors(t *testing.T) {
	setupRoleTestDB(t)

	team := seedTeamWithRoles(t, "team_patch", map[string]string{
		"manager@pingcap.com": models.TeamRoleManager,
		"editor@pingcap.com":  models.TeamRoleEditor,
		"member@pingcap.com":  models.TeamRoleMember,
	})

	s := &ScheduleService{}
	if err := s.UpdateTeam(team.ID, UpdateTeamRequest{Managers: []string{"member@pingcap.com"}}); err != nil {
		t.Fatalf("UpdateTeam: %v", err)
	}

	want := map[string]string{
		"member@pingcap.com":  models.TeamRoleManager, // newly named
		"manager@pingcap.com": models.TeamRoleMember,  // dropped from the list
		"editor@pingcap.com":  models.TeamRoleEditor,  // untouched
	}
	for email, wantRole := range want {
		got, err := s.TeamRoleOf(team.ID, email)
		if err != nil {
			t.Fatalf("%s: TeamRoleOf: %v", email, err)
		}
		if got != wantRole {
			t.Fatalf("%s: role = %q, want %q", email, got, wantRole)
		}
	}
}
