package rest

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/explorer/backend/api/freshness"
	"github.com/jackc/pgx/v5"
	"github.com/stretchr/testify/require"
)

type fakeStatsRow struct {
	scan func(dest ...any) error
}

func (r fakeStatsRow) Scan(dest ...any) error {
	return r.scan(dest...)
}

func TestLoadExplorerStatsReturnsValues(t *testing.T) {
	rpc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var req struct {
			Method string `json:"method"`
		}
		require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
		w.Header().Set("Content-Type", "application/json")
		switch req.Method {
		case "eth_blockNumber":
			_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"0x2c"}`))
		case "eth_getBlockByNumber":
			ts := time.Now().Add(-2 * time.Second).Unix()
			_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"timestamp":"0x` + strconv.FormatInt(ts, 16) + `"}}`))
		default:
			http.Error(w, `{"jsonrpc":"2.0","id":1,"error":{"message":"unsupported"}}`, http.StatusBadRequest)
		}
	}))
	defer rpc.Close()
	t.Setenv("RPC_URL", rpc.URL)
	var call int
	queryRow := func(_ context.Context, _ string, _ ...any) pgx.Row {
		call++
		return fakeStatsRow{
			scan: func(dest ...any) error {
				switch call {
				case 1:
					*dest[0].(*int64) = 11
				case 2:
					*dest[0].(*int64) = 22
				case 3:
					*dest[0].(*int64) = 33
				case 4:
					*dest[0].(*int64) = 44
				case 5:
					*dest[0].(*sql.NullFloat64) = sql.NullFloat64{Float64: 2000, Valid: true}
				case 6:
					*dest[0].(*sql.NullFloat64) = sql.NullFloat64{Float64: 1.25, Valid: true}
				case 7:
					*dest[0].(*sql.NullFloat64) = sql.NullFloat64{Float64: 37.5, Valid: true}
				case 8:
					*dest[0].(*sql.NullInt64) = sql.NullInt64{Int64: 18, Valid: true}
				case 9:
					*dest[0].(*int64) = 44
					*dest[1].(*time.Time) = time.Now().Add(-2 * time.Second)
				case 10:
					*dest[0].(*string) = "0xabc"
					*dest[1].(*int64) = 40
					*dest[2].(*time.Time) = time.Now().Add(-5 * time.Second)
				case 11:
					*dest[0].(*int64) = 40
					*dest[1].(*time.Time) = time.Now().Add(-5 * time.Second)
				case 12:
					*dest[0].(*int64) = 42
					*dest[1].(*time.Time) = time.Now().Add(-3 * time.Second)
				case 13:
					*dest[0].(*int64) = 128
					*dest[1].(*int64) = 10
					*dest[2].(*int64) = 22
				default:
					t.Fatalf("unexpected query call %d", call)
				}
				return nil
			},
		}
	}

	stats, err := loadExplorerStats(context.Background(), 138, queryRow)
	require.NoError(t, err)
	require.Equal(t, int64(11), stats.TotalBlocks)
	require.Equal(t, int64(22), stats.TotalTransactions)
	require.Equal(t, int64(33), stats.TotalAddresses)
	require.Equal(t, int64(44), stats.LatestBlock)
	require.NotNil(t, stats.AverageBlockTime)
	require.Equal(t, 2000.0, *stats.AverageBlockTime)
	require.NotNil(t, stats.GasPrices)
	require.NotNil(t, stats.GasPrices.Average)
	require.Equal(t, 1.25, *stats.GasPrices.Average)
	require.NotNil(t, stats.NetworkUtilizationPercentage)
	require.Equal(t, 37.5, *stats.NetworkUtilizationPercentage)
	require.NotNil(t, stats.TransactionsToday)
	require.Equal(t, int64(18), *stats.TransactionsToday)
	require.NotNil(t, stats.Freshness.ChainHead.BlockNumber)
	require.Equal(t, int64(40), *stats.Freshness.LatestIndexedTransaction.BlockNumber)
	require.Equal(t, int64(4), *stats.Freshness.LatestNonEmptyBlock.DistanceFromHead)
	require.Equal(t, "active", stats.Diagnostics.ActivityState)
	require.Equal(t, int64(4), *stats.Diagnostics.TxLagBlocks)
	require.Equal(t, "reported", string(stats.Freshness.ChainHead.Source))
	require.Equal(t, freshness.CompletenessComplete, stats.Completeness.GasMetrics)
	require.Equal(t, freshness.CompletenessComplete, stats.Completeness.UtilizationMetric)
}

func TestLoadExplorerStatsReturnsErrorWhenQueryFails(t *testing.T) {
	t.Setenv("RPC_URL", "")
	queryRow := func(_ context.Context, query string, _ ...any) pgx.Row {
		return fakeStatsRow{
			scan: func(dest ...any) error {
				if strings.Contains(query, "COUNT(*) FROM transactions") {
					return errors.New("boom")
				}
				target, ok := dest[0].(*int64)
				require.True(t, ok)
				*target = 1
				return nil
			},
		}
	}

	_, err := loadExplorerStats(context.Background(), 138, queryRow)
	require.Error(t, err)
	require.Contains(t, err.Error(), "query total transactions")
}
