package api

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strconv"
	"testing"

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

	"github.com/gin-gonic/gin"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// setupOverlayPermissionDB uses a shared-cache sqlite DSN (rather than plain
// ":memory:", which gives each pooled connection its own blank database and
// can deadlock/lose data once a handler needs more than one connection at a
// time within a request).
func setupOverlayPermissionDB(t *testing.T) {
	t.Helper()
	gin.SetMode(gin.TestMode)
	var err error
	db.DB, err = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	if err := db.DB.AutoMigrate(
		&models.Team{}, &models.Engineer{}, &models.EngineerTeam{}, &models.SuperAdmin{},
		&models.TeamLayer{}, &models.RealizedShift{}, &models.ShiftInterval{}, &models.AuditLog{},
	); err != nil {
		t.Fatalf("migrate: %v", err)
	}
}

// TestOverlayPermission_TeamMemberSelfAssign verifies the relaxed permission
// rule end to end: a team manager can reassign on-call to anyone, a plain
// team member may only reassign it to themselves, and a non-member can't
// touch it at all. Reverting an override (DELETE) stays manager-only.
func TestOverlayPermission_TeamMemberSelfAssign(t *testing.T) {
	setupOverlayPermissionDB(t)

	const managerEmail = "manager@pingcap.com"
	const memberEmail = "member@pingcap.com"
	const outsiderEmail = "outsider@pingcap.com"

	team := models.Team{Name: "team_perm", DisplayName: "Team Perm", GroupNames: models.StringList{"ONCALL"}}
	db.DB.Create(&team)
	manager := models.Engineer{Name: "Manager", Email: managerEmail}
	member := models.Engineer{Name: "Member", Email: memberEmail}
	db.DB.Create(&manager)
	db.DB.Create(&member)
	db.DB.Create(&models.EngineerTeam{EngineerID: manager.ID, TeamID: team.ID, Role: models.TeamRoleManager})
	db.DB.Create(&models.EngineerTeam{EngineerID: member.ID, TeamID: team.ID, Role: models.TeamRoleMember})

	r := gin.New()
	r.Use(func(c *gin.Context) {
		// Per-request identity via header, so a single test can exercise
		// different callers against the same router (unlike setupSmokeDB's
		// usual fixed-admin middleware).
		if email := c.GetHeader("X-Test-User"); email != "" {
			c.Set("request_user_email", email)
		}
		c.Next()
	})
	RegisterRoutes(r)

	do := func(method, url, asUser string, body interface{}) *httptest.ResponseRecorder {
		var rdr *bytes.Reader
		if body != nil {
			b, _ := json.Marshal(body)
			rdr = bytes.NewReader(b)
		} else {
			rdr = bytes.NewReader(nil)
		}
		req := httptest.NewRequest(method, url, rdr)
		req.Header.Set("Content-Type", "application/json")
		if asUser != "" {
			req.Header.Set("X-Test-User", asUser)
		}
		w := httptest.NewRecorder()
		r.ServeHTTP(w, req)
		return w
	}

	const start = int64(1704067200) // 2024-01-01 08:00 +08
	const end = start + 3600

	applyOverlay := func(asUser, engineerEmail string) *httptest.ResponseRecorder {
		return do(http.MethodPost, "/api/v1/overlays", asUser, map[string]interface{}{
			"type":           "segment",
			"team_id":        team.ID,
			"start_time":     start,
			"end_time":       end,
			"engineer_email": engineerEmail,
		})
	}

	// Plain team member reassigning the slot to themselves: allowed.
	if w := applyOverlay(memberEmail, memberEmail); w.Code != http.StatusOK {
		t.Fatalf("member self-assign: expected 200, got %d %s", w.Code, w.Body.String())
	}

	// Plain team member reassigning the slot to someone else: forbidden.
	if w := applyOverlay(memberEmail, managerEmail); w.Code != http.StatusForbidden {
		t.Fatalf("member assigning to someone else: expected 403, got %d %s", w.Code, w.Body.String())
	}

	// Non-member: forbidden even for self.
	if w := applyOverlay(outsiderEmail, outsiderEmail); w.Code != http.StatusForbidden {
		t.Fatalf("outsider self-assign: expected 403, got %d %s", w.Code, w.Body.String())
	}

	// Manager: can assign to anyone, including the member.
	if w := applyOverlay(managerEmail, memberEmail); w.Code != http.StatusOK {
		t.Fatalf("manager assigning to member: expected 200, got %d %s", w.Code, w.Body.String())
	}

	// Reverting an override stays manager-only: a plain member can't DELETE.
	revertURL := "/api/v1/overlays?team_id=" + strconv.Itoa(int(team.ID)) +
		"&start_time=" + strconv.FormatInt(start, 10) + "&end_time=" + strconv.FormatInt(end, 10)
	if w := do(http.MethodDelete, revertURL, memberEmail, nil); w.Code != http.StatusForbidden {
		t.Fatalf("member revert: expected 403, got %d %s", w.Code, w.Body.String())
	}
	if w := do(http.MethodDelete, revertURL, managerEmail, nil); w.Code != http.StatusOK {
		t.Fatalf("manager revert: expected 200, got %d %s", w.Code, w.Body.String())
	}
}
