package service

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"oci-portal/internal/model"
)

// newSystemLogEnv 建一个只含 SystemLog 表的内存库环境。
func newSystemLogEnv(t *testing.T) (*SystemLogService, *gorm.DB) {
	t.Helper()
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		t.Fatalf("open in-memory sqlite: %v", err)
	}
	// :memory: 库每个连接彼此独立，Record 的异步 goroutine 可能拿到新连接而丢表，锁单连接
	sqlDB, err := db.DB()
	if err != nil {
		t.Fatalf("db handle: %v", err)
	}
	sqlDB.SetMaxOpenConns(1)
	if err := db.AutoMigrate(&model.SystemLog{}); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	return NewSystemLogService(db), db
}

// seedLogs 由旧到新插入 n 条日志，时间逐分钟递增，路径 / 用户名带序号便于断言。
func seedLogs(t *testing.T, db *gorm.DB, n int) {
	t.Helper()
	base := time.Now().Add(-time.Duration(n) * time.Minute)
	for i := 0; i < n; i++ {
		row := model.SystemLog{
			Username:  fmt.Sprintf("user-%02d", i),
			Method:    "POST",
			Path:      fmt.Sprintf("/api/v1/res-%02d", i),
			Status:    200,
			CreatedAt: base.Add(time.Duration(i) * time.Minute),
		}
		if err := db.Create(&row).Error; err != nil {
			t.Fatalf("seed log %d: %v", i, err)
		}
	}
}

func TestSystemLogList(t *testing.T) {
	tests := []struct {
		name          string
		q             SystemLogQuery
		wantLen       int
		wantTotal     int64
		wantFirstPath string // 空串表示不校验首条
	}{
		{name: "零值分页取默认 20 条最新在前", q: SystemLogQuery{}, wantLen: 20, wantTotal: 25, wantFirstPath: "/api/v1/res-24"},
		{name: "第二页取余下 5 条", q: SystemLogQuery{Page: 2, PageSize: 20}, wantLen: 5, wantTotal: 25, wantFirstPath: "/api/v1/res-04"},
		{name: "pageSize 超上限被钳制仍能取全量", q: SystemLogQuery{PageSize: 200}, wantLen: 25, wantTotal: 25},
		{name: "关键字模糊匹配路径", q: SystemLogQuery{Keyword: "res-1"}, wantLen: 10, wantTotal: 10, wantFirstPath: "/api/v1/res-19"},
		{name: "关键字模糊匹配用户名", q: SystemLogQuery{Keyword: "user-05"}, wantLen: 1, wantTotal: 1, wantFirstPath: "/api/v1/res-05"},
		{name: "关键字无匹配返回空页", q: SystemLogQuery{Keyword: "no-such"}, wantLen: 0, wantTotal: 0},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc, db := newSystemLogEnv(t)
			seedLogs(t, db, 25)
			items, total, err := svc.List(context.Background(), tt.q)
			if err != nil {
				t.Fatalf("List: %v", err)
			}
			if len(items) != tt.wantLen || total != tt.wantTotal {
				t.Errorf("got %d items total %d, want %d items total %d", len(items), total, tt.wantLen, tt.wantTotal)
			}
			if tt.wantFirstPath != "" && items[0].Path != tt.wantFirstPath {
				t.Errorf("first path = %q, want %q", items[0].Path, tt.wantFirstPath)
			}
		})
	}
}

func TestSystemLogListStableOrder(t *testing.T) {
	svc, db := newSystemLogEnv(t)
	// 同一时间戳的两条记录按 id 倒序，保证翻页顺序稳定
	ts := time.Now().Truncate(time.Second)
	for i := 0; i < 2; i++ {
		row := model.SystemLog{Path: fmt.Sprintf("/p%d", i), CreatedAt: ts}
		if err := db.Create(&row).Error; err != nil {
			t.Fatalf("seed: %v", err)
		}
	}
	items, _, err := svc.List(context.Background(), SystemLogQuery{})
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	if len(items) != 2 || items[0].ID <= items[1].ID {
		t.Errorf("order = %+v, want id desc within same created_at", items)
	}
}

func TestSystemLogRecord(t *testing.T) {
	tests := []struct {
		name  string
		async bool
	}{
		{name: "record 同步落库", async: false},
		{name: "Record 异步落库", async: true},
	}
	entry := model.SystemLog{
		Username: "admin", Method: "PUT", Path: "/api/v1/oci-configs/:id",
		Status: 200, DurationMs: 18, ClientIP: "203.0.113.7",
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc, db := newSystemLogEnv(t)
			if tt.async {
				svc.Record(entry)
				svc.Wait()
			} else if err := svc.record(context.Background(), entry); err != nil {
				t.Fatalf("record: %v", err)
			}
			var got model.SystemLog
			if err := db.First(&got).Error; err != nil {
				t.Fatalf("load row: %v", err)
			}
			got.ID, got.CreatedAt = 0, entry.CreatedAt // 自动填充字段不参与对比
			if got != entry {
				t.Errorf("row = %+v, want %+v", got, entry)
			}
		})
	}
}

func TestSystemLogRecordFillsCreatedAt(t *testing.T) {
	svc, db := newSystemLogEnv(t)
	if err := svc.record(context.Background(), model.SystemLog{Path: "/p"}); err != nil {
		t.Fatalf("record: %v", err)
	}
	var got model.SystemLog
	if err := db.First(&got).Error; err != nil {
		t.Fatalf("load row: %v", err)
	}
	if got.CreatedAt.IsZero() {
		t.Error("createdAt is zero, want auto filled")
	}
}

func TestSystemLogCleanup(t *testing.T) {
	tests := []struct {
		name      string
		ages      []time.Duration // 每条记录距今的时长，由旧到新
		retention time.Duration
		maxRows   int
		wantPaths []string // 清理后应保留的路径（按时间升序编号 p0..pN）
	}{
		{
			name: "只删过期", ages: []time.Duration{100 * 24 * time.Hour, time.Hour},
			retention: 90 * 24 * time.Hour, maxRows: 10, wantPaths: []string{"/p1"},
		},
		{
			name: "超上限删最旧", ages: []time.Duration{5 * time.Hour, 4 * time.Hour, 3 * time.Hour, 2 * time.Hour, time.Hour},
			retention: 90 * 24 * time.Hour, maxRows: 3, wantPaths: []string{"/p2", "/p3", "/p4"},
		},
		{
			name: "都未触发保持原样", ages: []time.Duration{2 * time.Hour, time.Hour},
			retention: 90 * 24 * time.Hour, maxRows: 10, wantPaths: []string{"/p0", "/p1"},
		},
		{
			name: "过期与超量叠加", ages: []time.Duration{100 * 24 * time.Hour, 4 * time.Hour, 3 * time.Hour, 2 * time.Hour, time.Hour},
			retention: 90 * 24 * time.Hour, maxRows: 2, wantPaths: []string{"/p3", "/p4"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			svc, db := newSystemLogEnv(t)
			for i, age := range tt.ages {
				row := model.SystemLog{Path: fmt.Sprintf("/p%d", i), CreatedAt: time.Now().Add(-age)}
				if err := db.Create(&row).Error; err != nil {
					t.Fatalf("seed %d: %v", i, err)
				}
			}
			if err := svc.cleanup(context.Background(), tt.retention, tt.maxRows); err != nil {
				t.Fatalf("cleanup: %v", err)
			}
			var paths []string
			if err := db.Model(&model.SystemLog{}).Order("created_at ASC").Pluck("path", &paths).Error; err != nil {
				t.Fatalf("load rows: %v", err)
			}
			if fmt.Sprint(paths) != fmt.Sprint(tt.wantPaths) {
				t.Errorf("kept = %v, want %v", paths, tt.wantPaths)
			}
		})
	}
}

func TestSystemLogStartCleanupStopsOnCancel(t *testing.T) {
	svc, _ := newSystemLogEnv(t)
	ctx, cancel := context.WithCancel(context.Background())
	svc.StartCleanup(ctx)
	cancel()
	done := make(chan struct{})
	go func() {
		svc.Wait()
		close(done)
	}()
	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("cleanup goroutine did not exit after context cancel")
	}
}
