package api

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

	oncallapi "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/oncall"
	"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"
)

func setupSmokeDB(t *testing.T) {
	t.Helper()
	gin.SetMode(gin.TestMode)
	var err error
	db.DB, err = gorm.Open(sqlite.Open(":memory:"), &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)
	}
}

func TestLayerEndToEndSmoke(t *testing.T) {
	setupSmokeDB(t)
	admin := "admin@pingcap.com"
	db.DB.Create(&models.SuperAdmin{Email: admin, CreatedBy: "test"})
	cat := models.Team{Name: "team_x", DisplayName: "Team X", GroupNames: models.StringList{"ONCALL"}}
	db.DB.Create(&cat)
	for _, e := range []*models.Engineer{
		{Name: "Alice", Email: "a@pingcap.com"},
		{Name: "Bob", Email: "b@pingcap.com"},
		{Name: "Carol", Email: "c@pingcap.com"},
	} {
		db.DB.Create(e)
		if e.Email == "a@pingcap.com" {
			db.DB.Create(&models.EngineerTeam{EngineerID: e.ID, TeamID: cat.ID, Role: models.TeamRoleManager})
		}
	}

	r := gin.New()
	r.Use(func(c *gin.Context) {
		c.Set("request_user_email", admin)
		c.Set("request_user_name", "admin")
		c.Next()
	})
	RegisterRoutes(r)

	do := func(method, url 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")
		w := httptest.NewRecorder()
		r.ServeHTTP(w, req)
		return w
	}

	const T0 = int64(1704038400) // 2024-01-01 00:00 +08
	const wk = int64(7 * 24 * 3600)

	// 1. Create a weekly rotation layer. handoff_at anchors the handover grid
	// at T0 with the first person (Alice) heading the slot that begins there.
	w := do(http.MethodPut, "/api/v1/teams/"+strconv.Itoa(int(cat.ID))+"/layers", map[string]interface{}{
		"layers": []map[string]interface{}{{
			"priority":             1,
			"people":               []string{"a@pingcap.com", "b@pingcap.com", "c@pingcap.com"},
			"rotate_every_seconds": wk,
			"on_call_at_once":      1,
			"active_mode":          "always",
			"handoff_at":           T0,
		}},
	})
	if w.Code != http.StatusOK {
		t.Fatalf("POST layers: %d %s", w.Code, w.Body.String())
	}

	// 1b. Dry-run preview must not require an existing schedule and not write.
	w = do(http.MethodPost, "/api/v1/teams/"+strconv.Itoa(int(cat.ID))+"/layers/preview", map[string]interface{}{
		"layers": []map[string]interface{}{{
			"priority":             1,
			"people":               []string{"a@pingcap.com", "x@pingcap.com"},
			"rotate_every_seconds": wk,
			"on_call_at_once":      1,
			"active_mode":          "always",
		}},
	})
	if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "not a member of this team") {
		t.Fatalf("preview: %d %s", w.Code, w.Body.String())
	}

	// 2. Read back.
	w = do(http.MethodGet, "/api/v1/teams/"+strconv.Itoa(int(cat.ID))+"/layers", nil)
	if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "a@pingcap.com") {
		t.Fatalf("GET layers: %d %s", w.Code, w.Body.String())
	}

	// 2b. Resolve by slug name (shareable URL path).
	w = do(http.MethodGet, "/api/v1/teams/team_x", nil)
	if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "\"team\"") {
		t.Fatalf("teamByName: %d %s", w.Code, w.Body.String())
	}

	// 3. Effective window over 3 weeks -> a, b, c.
	url := "/api/v1/schedule?team_ids=" + strconv.Itoa(int(cat.ID)) +
		"&start_time=" + strconv.FormatInt(T0, 10) + "&end_time=" + strconv.FormatInt(T0+3*wk, 10)
	w = do(http.MethodGet, url, nil)
	body := w.Body.String()
	if w.Code != http.StatusOK {
		t.Fatalf("effective: %d %s", w.Code, body)
	}
	for _, want := range []string{"a@pingcap.com", "b@pingcap.com", "c@pingcap.com"} {
		if !strings.Contains(body, want) {
			t.Fatalf("effective missing %s: %s", want, body)
		}
	}

	// 4. Current on-call at week 0 -> Alice.
	w = do(http.MethodGet, "/api/v1/teams/"+strconv.Itoa(int(cat.ID))+"/current-oncall?at_time="+strconv.FormatInt(T0+3600, 10), nil)
	if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "a@pingcap.com") {
		t.Fatalf("current: %d %s", w.Code, w.Body.String())
	}
	var current oncallapi.CurrentOncallResponse
	if err := json.Unmarshal(w.Body.Bytes(), &current); err != nil {
		t.Fatalf("decode typed current-oncall response: %v", err)
	}
	if current.TeamID != cat.ID || current.AtTime != T0+3600 {
		t.Fatalf("unexpected typed current-oncall response: %+v", current)
	}
	if len(current.ManagerEmails) != 1 || current.ManagerEmails[0] != "a@pingcap.com" {
		t.Fatalf("unexpected manager emails: %+v", current.ManagerEmails)
	}

	w = do(http.MethodGet, "/api/v1/teams/"+strconv.Itoa(int(cat.ID))+"/current-oncall?at_time=invalid", nil)
	if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), ErrorCodeInvalidArgument) {
		t.Fatalf("invalid current time: %d %s", w.Code, w.Body.String())
	}

	// 5. Notify preview lists the on-call engineer.
	w = do(http.MethodPost, "/api/v1/notifications/feishu/preview", map[string]interface{}{
		"group_name":           "ONCALL",
		"at_time":              T0 + wk + 3600, // week 1 -> Bob
		"exclude_test_teams":   false,
		"skip_same_substitute": false,
	})
	if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "Bob") {
		t.Fatalf("notify: %d %s", w.Code, w.Body.String())
	}
}
