package registry

import (
	"context"
	"database/sql"
	"sync/atomic"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
)

func setupHibernationMock(t *testing.T) sqlmock.Sqlmock {
	t.Helper()
	mockDB, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	db.DB = mockDB
	t.Cleanup(func() { mockDB.Close() })
	return mock
}

// TestHibernateIdleWorkspaces_CallsHandlerForEachCandidate verifies that
// hibernateIdleWorkspaces calls onHibernate once for each workspace row
// returned by the DB query.
func TestHibernateIdleWorkspaces_CallsHandlerForEachCandidate(t *testing.T) {
	mock := setupHibernationMock(t)

	mock.ExpectQuery(`SELECT id FROM workspaces`).
		WillReturnRows(sqlmock.NewRows([]string{"id"}).
			AddRow("ws-idle-1").
			AddRow("ws-idle-2"))

	var called []string
	hibernateIdleWorkspaces(context.Background(), func(ctx context.Context, id string) {
		called = append(called, id)
	})

	if len(called) != 2 {
		t.Fatalf("expected 2 hibernations, got %d: %v", len(called), called)
	}
	if called[0] != "ws-idle-1" || called[1] != "ws-idle-2" {
		t.Errorf("unexpected IDs: %v", called)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet expectations: %v", err)
	}
}

// TestHibernateIdleWorkspaces_NoRowsNoHandler verifies that no handler is
// called when the query returns zero rows (no idle workspaces).
func TestHibernateIdleWorkspaces_NoRowsNoHandler(t *testing.T) {
	mock := setupHibernationMock(t)

	mock.ExpectQuery(`SELECT id FROM workspaces`).
		WillReturnRows(sqlmock.NewRows([]string{"id"})) // empty

	var called int
	hibernateIdleWorkspaces(context.Background(), func(_ context.Context, _ string) {
		called++
	})

	if called != 0 {
		t.Errorf("expected 0 hibernations, got %d", called)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet expectations: %v", err)
	}
}

// TestHibernateIdleWorkspaces_DBErrorDoesNotPanic verifies that a DB error
// from the query is logged but does not crash the monitor loop.
func TestHibernateIdleWorkspaces_DBErrorDoesNotPanic(t *testing.T) {
	mock := setupHibernationMock(t)

	mock.ExpectQuery(`SELECT id FROM workspaces`).
		WillReturnError(sql.ErrConnDone)

	// Should not panic
	hibernateIdleWorkspaces(context.Background(), func(_ context.Context, _ string) {
		t.Error("handler should not be called on DB error")
	})

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet expectations: %v", err)
	}
}

// TestStartHibernationMonitor_TicksAndCallsHandler verifies the monitor loop
// ticks at the configured interval and calls the handler.
func TestStartHibernationMonitor_TicksAndCallsHandler(t *testing.T) {
	mock := setupHibernationMock(t)

	// Expect at least one DB query (the first tick)
	mock.ExpectQuery(`SELECT id FROM workspaces`).
		WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("ws-hibernate-me"))

	var callCount int32
	ctx, cancel := context.WithCancel(context.Background())

	done := make(chan struct{})
	go func() {
		StartHibernationMonitorWithInterval(ctx, 50*time.Millisecond, func(_ context.Context, id string) {
			if id == "ws-hibernate-me" {
				atomic.AddInt32(&callCount, 1)
				cancel() // stop after first hit
			}
		})
		close(done)
	}()

	select {
	case <-done:
	case <-time.After(3 * time.Second):
		t.Fatal("monitor did not stop within timeout")
	}

	if atomic.LoadInt32(&callCount) == 0 {
		t.Error("expected handler to be called at least once")
	}
}

// TestStartHibernationMonitor_StopsOnContextCancel verifies clean shutdown
// when the context is cancelled before any tick fires.
func TestStartHibernationMonitor_StopsOnContextCancel(t *testing.T) {
	_ = setupHibernationMock(t) // no DB calls expected

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // cancel immediately

	done := make(chan struct{})
	go func() {
		// Very long interval — only context cancel should stop it
		StartHibernationMonitorWithInterval(ctx, 10*time.Minute, func(_ context.Context, _ string) {
			// should never be called
		})
		close(done)
	}()

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatal("monitor did not stop on context cancel")
	}
}
