package api

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

	"github.com/alicebob/miniredis/v2"
	"github.com/ez-api/ez-api/internal/model"
	"github.com/ez-api/ez-api/internal/service"
	"github.com/gin-gonic/gin"
	"github.com/redis/go-redis/v9"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func newTestHandlerWithRedis(t *testing.T) (*Handler, *gorm.DB, *miniredis.Miniredis) {
	t.Helper()
	gin.SetMode(gin.TestMode)

	dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}
	if err := db.AutoMigrate(&model.ProviderGroup{}, &model.APIKey{}, &model.Binding{}, &model.Model{}); err != nil {
		t.Fatalf("migrate: %v", err)
	}

	mr := miniredis.RunT(t)
	rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
	sync := service.NewSyncService(rdb)
	return NewHandler(db, db, sync, nil, rdb, nil), db, mr
}

func TestCreateModel_DefaultsKindChat_AndWritesModelsMeta(t *testing.T) {
	h, _, mr := newTestHandlerWithRedis(t)

	r := gin.New()
	r.POST("/admin/models", h.CreateModel)

	reqBody := map[string]any{
		"name": "ns.m",
	}
	b, _ := json.Marshal(reqBody)

	req := httptest.NewRequest(http.MethodPost, "/admin/models", bytes.NewReader(b))
	req.Header.Set("Content-Type", "application/json")
	rr := httptest.NewRecorder()
	r.ServeHTTP(rr, req)

	if rr.Code != http.StatusCreated {
		t.Fatalf("expected 201, got %d body=%s", rr.Code, rr.Body.String())
	}

	raw := mr.HGet("meta:models", "ns.m")
	if raw == "" {
		t.Fatalf("expected meta:models[ns.m] to be written")
	}
	var snap map[string]any
	if err := json.Unmarshal([]byte(raw), &snap); err != nil {
		t.Fatalf("unmarshal snapshot: %v raw=%s", err, raw)
	}
	if snap["kind"] != "chat" {
		t.Fatalf("expected kind=chat, got %v raw=%s", snap["kind"], raw)
	}

	if v := mr.HGet("meta:models_meta", "version"); v == "" {
		t.Fatalf("expected meta:models_meta.version")
	}
	if v := mr.HGet("meta:models_meta", "updated_at"); v == "" {
		t.Fatalf("expected meta:models_meta.updated_at")
	}
	if v := mr.HGet("meta:models_meta", "source"); v == "" {
		t.Fatalf("expected meta:models_meta.source")
	}
	if v := mr.HGet("meta:models_meta", "checksum"); v == "" {
		t.Fatalf("expected meta:models_meta.checksum")
	}
}

func TestCreateModel_InvalidKind_Returns400(t *testing.T) {
	h, _, _ := newTestHandlerWithRedis(t)

	r := gin.New()
	r.POST("/admin/models", h.CreateModel)

	reqBody := map[string]any{
		"name": "ns.m2",
		"kind": "bad",
	}
	b, _ := json.Marshal(reqBody)

	req := httptest.NewRequest(http.MethodPost, "/admin/models", bytes.NewReader(b))
	req.Header.Set("Content-Type", "application/json")
	rr := httptest.NewRecorder()
	r.ServeHTTP(rr, req)

	if rr.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d body=%s", rr.Code, rr.Body.String())
	}
}

func TestDeleteModel_RemovesMeta(t *testing.T) {
	h, db, mr := newTestHandlerWithRedis(t)

	r := gin.New()
	r.POST("/admin/models", h.CreateModel)
	r.DELETE("/admin/models/:id", h.DeleteModel)

	reqBody := map[string]any{
		"name": "ns.del",
	}
	b, _ := json.Marshal(reqBody)

	req := httptest.NewRequest(http.MethodPost, "/admin/models", bytes.NewReader(b))
	req.Header.Set("Content-Type", "application/json")
	rr := httptest.NewRecorder()
	r.ServeHTTP(rr, req)

	if rr.Code != http.StatusCreated {
		t.Fatalf("expected 201, got %d body=%s", rr.Code, rr.Body.String())
	}
	var created model.Model
	if err := json.Unmarshal(rr.Body.Bytes(), &created); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}

	delReq := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/models/%d", created.ID), nil)
	delRec := httptest.NewRecorder()
	r.ServeHTTP(delRec, delReq)

	if delRec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", delRec.Code, delRec.Body.String())
	}
	if raw := mr.HGet("meta:models", "ns.del"); raw != "" {
		t.Fatalf("expected meta:models[ns.del] removed, got %q", raw)
	}
	var remaining int64
	if err := db.Model(&model.Model{}).Where("name = ?", "ns.del").Count(&remaining).Error; err != nil {
		t.Fatalf("count: %v", err)
	}
	if remaining != 0 {
		t.Fatalf("expected model deleted, got count=%d", remaining)
	}
}

func TestBatchModels_Delete(t *testing.T) {
	h, db, mr := newTestHandlerWithRedis(t)

	r := gin.New()
	r.POST("/admin/models", h.CreateModel)
	r.POST("/admin/models/batch", h.BatchModels)

	create := func(name string) uint {
		reqBody := map[string]any{"name": name}
		b, _ := json.Marshal(reqBody)
		req := httptest.NewRequest(http.MethodPost, "/admin/models", bytes.NewReader(b))
		req.Header.Set("Content-Type", "application/json")
		rr := httptest.NewRecorder()
		r.ServeHTTP(rr, req)
		if rr.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d body=%s", rr.Code, rr.Body.String())
		}
		var created model.Model
		if err := json.Unmarshal(rr.Body.Bytes(), &created); err != nil {
			t.Fatalf("unmarshal: %v", err)
		}
		return created.ID
	}

	id1 := create("ns.b1")
	id2 := create("ns.b2")

	payload := map[string]any{
		"action": "delete",
		"ids":    []uint{id1, id2},
	}
	b, _ := json.Marshal(payload)
	req := httptest.NewRequest(http.MethodPost, "/admin/models/batch", bytes.NewReader(b))
	req.Header.Set("Content-Type", "application/json")
	rr := httptest.NewRecorder()
	r.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
	}

	var remaining int64
	if err := db.Model(&model.Model{}).Count(&remaining).Error; err != nil {
		t.Fatalf("count: %v", err)
	}
	if remaining != 0 {
		t.Fatalf("expected models deleted, got %d", remaining)
	}
	if got := mr.HGet("meta:models", "ns.b1"); got != "" {
		t.Fatalf("expected meta removed, got %q", got)
	}
}

func TestBatchBindings_Status(t *testing.T) {
	h, db, _ := newTestHandlerWithRedis(t)

	group := model.ProviderGroup{Name: "default", Type: "openai", BaseURL: "https://api.openai.com/v1", Models: "m1", Status: "active"}
	if err := db.Create(&group).Error; err != nil {
		t.Fatalf("create group: %v", err)
	}
	if err := db.Create(&model.APIKey{GroupID: group.ID, APIKey: "k", Status: "active"}).Error; err != nil {
		t.Fatalf("create api key: %v", err)
	}
	b := &model.Binding{
		Namespace:     "ns",
		PublicModel:   "m1",
		GroupID:       group.ID,
		Weight:        1,
		SelectorType:  "exact",
		SelectorValue: "m1",
		Status:        "active",
	}
	if err := db.Create(b).Error; err != nil {
		t.Fatalf("create binding: %v", err)
	}

	r := gin.New()
	r.POST("/admin/bindings/batch", h.BatchBindings)

	payload := map[string]any{
		"action": "status",
		"status": "inactive",
		"ids":    []uint{b.ID},
	}
	bb, _ := json.Marshal(payload)
	req := httptest.NewRequest(http.MethodPost, "/admin/bindings/batch", bytes.NewReader(bb))
	req.Header.Set("Content-Type", "application/json")
	rr := httptest.NewRecorder()
	r.ServeHTTP(rr, req)

	if rr.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
	}
	var updated model.Binding
	if err := db.First(&updated, b.ID).Error; err != nil {
		t.Fatalf("reload binding: %v", err)
	}
	if updated.Status != "inactive" {
		t.Fatalf("expected status inactive, got %q", updated.Status)
	}
}
