package main

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

	"github.com/DATA-DOG/go-sqlmock"
)

func TestDeliverySelectorFlagKeepsColonInsideEventID(t *testing.T) {
	var values deliverySelectorFlag
	if err := values.Set("wallet_outbox: FAMI :wallet:balance:evt-1"); err != nil {
		t.Fatalf("parse exact delivery selector: %v", err)
	}
	if len(values) != 1 || values[0].SourceName != walletSource || values[0].AppCode != "fami" || values[0].EventID != "wallet:balance:evt-1" {
		t.Fatalf("parsed selector mismatch: %+v", values)
	}
	if err := values.Set("unknown:lalu:event-2"); err == nil {
		t.Fatal("unsupported source must be rejected before any database access")
	}
	if err := values.Set("room_outbox::event-3"); err == nil {
		t.Fatal("empty app_code must not default to lalu")
	}
}

func TestLoadExactDueDeliveriesUsesFullPrimaryKey(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sqlmock: %v", err)
	}
	defer db.Close()
	nowMS := int64(1_700_000_000_000)
	query := regexp.QuoteMeta(`
			SELECT source_name, app_code, source_event_id, status, retry_count, updated_at_ms
			FROM notice_delivery_events FORCE INDEX (PRIMARY)
			WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?
			  AND ((status = 'retryable' AND next_retry_at_ms <= ?)
			       OR (status = 'delivering' AND lock_until_ms <= ?))`)
	mock.ExpectQuery(query).
		WithArgs(walletSource, "fami", "wallet:evt-1", c2cChannel, nowMS, nowMS).
		WillReturnRows(sqlmock.NewRows([]string{"source_name", "app_code", "source_event_id", "status", "retry_count", "updated_at_ms"}).
			AddRow(walletSource, "fami", "wallet:evt-1", "retryable", 2, nowMS-1000))

	candidates, err := loadExactDueDeliveries(context.Background(), db, []deliverySelector{{SourceName: walletSource, AppCode: " FAMI ", EventID: "wallet:evt-1"}}, nowMS)
	if err != nil {
		t.Fatalf("load exact due delivery: %v", err)
	}
	if len(candidates) != 1 || candidates[0].Selector != "wallet_outbox:fami:wallet:evt-1" || candidates[0].RetryCount != 2 {
		t.Fatalf("exact candidate mismatch: %+v", candidates)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("exact query did not use the expected primary key: %v", err)
	}
}

func TestLoadExactDueDeliveriesRejectsDuplicateBeforeQuery(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sqlmock: %v", err)
	}
	defer db.Close()
	selectors := []deliverySelector{
		{SourceName: roomSource, AppCode: "lalu", EventID: "evt-1"},
		{SourceName: roomSource, AppCode: " LALU ", EventID: "evt-1"},
	}
	if _, err := loadExactDueDeliveries(context.Background(), db, selectors, 1000); err == nil || !strings.Contains(err.Error(), "duplicate") {
		t.Fatalf("duplicate selector error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("duplicate validation unexpectedly queried MySQL: %v", err)
	}
}

func TestLoadExactDueDeliveriesRejectsNotDueRow(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sqlmock: %v", err)
	}
	defer db.Close()
	mock.ExpectQuery("FROM notice_delivery_events FORCE INDEX \\(PRIMARY\\)").
		WillReturnError(sql.ErrNoRows)
	_, err = loadExactDueDeliveries(context.Background(), db, []deliverySelector{{SourceName: roomSource, AppCode: "lalu", EventID: "evt-1"}}, 1000)
	if err == nil || !strings.Contains(err.Error(), "no longer due") {
		t.Fatalf("not-due selector error = %v", err)
	}
}

func TestDueDeliveryBranchUsesCompleteRetryIndexPrefix(t *testing.T) {
	query, args := dueDeliveryBranch(walletSource, "huwaa", "idx_notice_delivery_retry", "retryable", "next_retry_at_ms", []string{"evt-1", "evt-2"}, 20, 1234)
	for _, fragment := range []string{
		"FORCE INDEX (idx_notice_delivery_retry)",
		"source_name = ? AND app_code = ? AND channel = ? AND status = ?",
		"next_retry_at_ms <= ?",
		"source_event_id IN (?,?)",
	} {
		if !strings.Contains(query, fragment) {
			t.Fatalf("indexed preview query is missing %q:\n%s", fragment, query)
		}
	}
	wantArgs := []any{walletSource, "huwaa", c2cChannel, "retryable", int64(1234), "evt-1", "evt-2", 20}
	if len(args) != len(wantArgs) {
		t.Fatalf("preview args = %v, want %v", args, wantArgs)
	}
	for index := range args {
		if args[index] != wantArgs[index] {
			t.Fatalf("preview arg[%d] = %#v, want %#v", index, args[index], wantArgs[index])
		}
	}
}
