// Package mysqltest provides real room-service MySQL repositories for tests.
package mysqltest

import (
	"context"
	"database/sql"
	"testing"

	"google.golang.org/protobuf/proto"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	"hyapp/internal/testutil/mysqlschema"
	"hyapp/pkg/appcode"
	"hyapp/services/room-service/internal/room/outbox"
	mysqlstorage "hyapp/services/room-service/internal/storage/mysql"
)

// Repository wraps the production room MySQL repository with test-only read helpers.
type Repository struct {
	*mysqlstorage.Repository

	t      testing.TB
	schema *mysqlschema.Schema
}

// NewRepository creates an isolated room schema and opens the production repository.
func NewRepository(t testing.TB) *Repository {
	t.Helper()

	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "ROOM_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     initDBPath(t),
		DatabasePrefix: "hyapp_room_test",
	})
	repository, err := mysqlstorage.Open(context.Background(), schema.DSN, 4, 4)
	if err != nil {
		t.Fatalf("open room mysql repository failed: %v", err)
	}

	wrapped := &Repository{Repository: repository, t: t, schema: schema}
	t.Cleanup(wrapped.Close)
	return wrapped
}

// Close shuts down the repository; mysqlschema owns schema cleanup.
func (r *Repository) Close() {
	r.t.Helper()

	if r.Repository != nil {
		_ = r.Repository.Close()
	}
}

// SetRoomStatus simulates an external admin status update against room-service tables.
func (r *Repository) SetRoomStatus(roomID string, status string) {
	r.t.Helper()

	ctx := context.Background()
	if _, err := r.schema.DB.ExecContext(ctx, `UPDATE rooms SET status = ? WHERE app_code = ? AND room_id = ?`, status, appcode.Default, roomID); err != nil {
		r.t.Fatalf("update room status failed: %v", err)
	}
	if _, err := r.schema.DB.ExecContext(ctx, `UPDATE room_list_entries SET status = ? WHERE app_code = ? AND room_id = ?`, status, appcode.Default, roomID); err != nil {
		r.t.Fatalf("update room list status failed: %v", err)
	}
}

// DeleteRoomListEntry removes the discovery projection while keeping rooms metadata.
func (r *Repository) DeleteRoomListEntry(roomID string) {
	r.t.Helper()

	if _, err := r.schema.DB.ExecContext(context.Background(), `DELETE FROM room_list_entries WHERE app_code = ? AND room_id = ?`, appcode.Default, roomID); err != nil {
		r.t.Fatalf("delete room list entry failed: %v", err)
	}
}

// CountRowsByRoom returns how many rows a room still owns in a test table.
func (r *Repository) CountRowsByRoom(table string, roomID string) int64 {
	r.t.Helper()

	var count int64
	if err := r.schema.DB.QueryRowContext(context.Background(), `SELECT COUNT(*) FROM `+table+` WHERE app_code = ? AND room_id = ?`, appcode.Default, roomID).Scan(&count); err != nil {
		r.t.Fatalf("count %s rows failed: %v", table, err)
	}
	return count
}

// SetRoomSortScore rewrites discovery sort_score for deterministic list-order tests.
func (r *Repository) SetRoomSortScore(roomID string, sortScore int64) {
	r.t.Helper()

	r.requireRoomListEntry(roomID)
	if _, err := r.schema.DB.ExecContext(context.Background(), `UPDATE room_list_entries SET sort_score = ? WHERE app_code = ? AND room_id = ?`, sortScore, appcode.Default, roomID); err != nil {
		r.t.Fatalf("update room sort score failed: %v", err)
	}
}

// SetRoomPresence rewrites discovery presence counters for deterministic hot bucket tests.
func (r *Repository) SetRoomPresence(roomID string, onlineCount int32, occupiedSeatCount int32) {
	r.t.Helper()

	r.requireRoomListEntry(roomID)
	if _, err := r.schema.DB.ExecContext(context.Background(), `UPDATE room_list_entries SET online_count = ?, occupied_seat_count = ? WHERE app_code = ? AND room_id = ?`, onlineCount, occupiedSeatCount, appcode.Default, roomID); err != nil {
		r.t.Fatalf("update room presence failed: %v", err)
	}
}

func (r *Repository) requireRoomListEntry(roomID string) {
	r.t.Helper()

	var exists int
	if err := r.schema.DB.QueryRowContext(context.Background(), `SELECT 1 FROM room_list_entries WHERE app_code = ? AND room_id = ? LIMIT 1`, appcode.Default, roomID).Scan(&exists); err != nil {
		r.t.Fatalf("room list entry %s is missing: %v", roomID, err)
	}
}

// PinRoom inserts an active regional pin exactly as the admin backend writes it.
func (r *Repository) PinRoom(roomID string, regionID int64, weight int64, expiresAtMS int64) {
	r.PinRoomWithType(roomID, "region", regionID, weight, expiresAtMS)
}

// PinRoomWithType inserts an active pin with the same scope fields used by admin writes.
func (r *Repository) PinRoomWithType(roomID string, pinType string, regionID int64, weight int64, expiresAtMS int64) {
	r.t.Helper()

	nowMS := int64(1000)
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO room_region_pins (
		  app_code, visible_region_id, pin_type, room_id, weight, status, pinned_at_ms, expires_at_ms,
		  cancelled_at_ms, created_by_admin_id, cancelled_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, 0, 1, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
		  weight = VALUES(weight),
		  status = VALUES(status),
		  pin_type = VALUES(pin_type),
		  pinned_at_ms = VALUES(pinned_at_ms),
		  expires_at_ms = VALUES(expires_at_ms),
		  cancelled_at_ms = 0,
		  updated_at_ms = VALUES(updated_at_ms)
	`, appcode.Default, regionID, pinType, roomID, weight, nowMS, expiresAtMS, nowMS, nowMS); err != nil {
		r.t.Fatalf("pin room failed: %v", err)
	}
}

// SetRoomCreatedAt rewrites discovery created_at_ms for deterministic new-tab order tests.
func (r *Repository) SetRoomCreatedAt(roomID string, createdAtMS int64) {
	r.t.Helper()

	if _, err := r.schema.DB.ExecContext(context.Background(), `UPDATE room_list_entries SET created_at_ms = ? WHERE app_code = ? AND room_id = ?`, createdAtMS, appcode.Default, roomID); err != nil {
		r.t.Fatalf("update room created_at_ms failed: %v", err)
	}
}

// SetOutboxDeliveredAt rewrites one outbox row to delivered with a fixed updated_at_ms for retention tests.
func (r *Repository) SetOutboxDeliveredAt(eventID string, updatedAtMS int64) {
	r.t.Helper()

	result, err := r.schema.DB.ExecContext(context.Background(),
		`UPDATE room_outbox SET status = ?, worker_id = '', lock_until_ms = NULL, updated_at_ms = ? WHERE app_code = ? AND event_id = ?`,
		outbox.StatusDelivered, updatedAtMS, appcode.Default, eventID)
	if err != nil {
		r.t.Fatalf("mark outbox delivered failed: %v", err)
	}
	if affected, err := result.RowsAffected(); err != nil || affected != 1 {
		r.t.Fatalf("outbox row %s not rewritten (affected=%d err=%v)", eventID, affected, err)
	}
}

// OutboxRecord reads one outbox row, including delivered rows that normal scans skip.
func (r *Repository) OutboxRecord(eventID string) (outbox.Record, bool) {
	r.t.Helper()

	return r.outboxRecord("room_outbox", eventID)
}

func (r *Repository) outboxRecord(table string, eventID string) (outbox.Record, bool) {
	r.t.Helper()

	row := r.schema.DB.QueryRowContext(context.Background(), `
		SELECT app_code, event_id, event_type, room_id, status, worker_id, lock_until_ms, envelope, created_at_ms, retry_count, next_retry_at_ms, COALESCE(last_error, '')
		FROM `+table+`
		WHERE app_code = ? AND event_id = ?
	`, appcode.Default, eventID)

	var envelopeBytes []byte
	var record outbox.Record
	var lockUntil sql.Null[int64]
	if err := row.Scan(&record.AppCode, &record.EventID, &record.EventType, &record.RoomID, &record.Status, &record.WorkerID, &lockUntil, &envelopeBytes, &record.CreatedAtMS, &record.RetryCount, &record.NextRetryAtMS, &record.LastError); err != nil {
		if err == sql.ErrNoRows {
			return outbox.Record{}, false
		}
		r.t.Fatalf("query room outbox record failed: %v", err)
	}
	if lockUntil.Valid {
		record.LockUntilMS = lockUntil.V
	}

	var envelope roomeventsv1.EventEnvelope
	if err := proto.Unmarshal(envelopeBytes, &envelope); err != nil {
		r.t.Fatalf("decode room outbox envelope failed: %v", err)
	}
	record.Envelope = &envelope

	return record, true
}

func initDBPath(t testing.TB) string {
	t.Helper()

	return mysqlschema.InitDBPath(t,
		mysqlschema.CallerFile(t, 1),
		"..", "..", "..", "deploy", "mysql", "initdb", "001_room_service.sql",
	)
}
