package storage

import (
	"fmt"
	"math/rand"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
)

func TestMain(m *testing.M) {
	isDebug = true
	n := m.Run()
	fs.MustRemoveDir("benchmarkTableSearch")
	os.Exit(n)
}

func BenchmarkTableSearch(b *testing.B) {
	for _, rowsCount := range []int{1e5, 1e6, 1e7, 1e8} {
		b.Run(fmt.Sprintf("rowsCount_%d", rowsCount), func(b *testing.B) {
			for _, tsidsCount := range []int{1e3, 1e4} {
				b.Run(fmt.Sprintf("tsidsCount_%d", tsidsCount), func(b *testing.B) {
					for _, tsidsSearch := range []int{1, 1e1, 1e2, 1e3, 1e4} {
						b.Run(fmt.Sprintf("tsidsSearch_%d", tsidsSearch), func(b *testing.B) {
							benchmarkTableSearch(b, rowsCount, tsidsCount, tsidsSearch)
						})
					}
				})
			}
		})
	}
}

func openBenchTable(b *testing.B, startTimestamp int64, rowsPerInsert, rowsCount, tsidsCount int) (*table, *Storage) {
	b.Helper()

	path := filepath.Join("benchmarkTableSearch", fmt.Sprintf("rows%d_tsids%d", rowsCount, tsidsCount))
	if !createdBenchTables[path] {
		createBenchTable(b, path, startTimestamp, rowsPerInsert, rowsCount, tsidsCount)
		createdBenchTables[path] = true
	}
	strg := newTestStorage()
	tb := mustOpenTable(path, strg)

	// Verify rows count in the table opened from files.
	insertsCount := uint64((rowsCount + rowsPerInsert - 1) / rowsPerInsert)
	rowsCountExpected := insertsCount * uint64(rowsPerInsert)
	var m TableMetrics
	tb.UpdateMetrics(&m)
	if rowsCount := m.TotalRowsCount(); rowsCount != rowsCountExpected {
		b.Fatalf("unexpected rows count in the table %q; got %d; want %d", path, rowsCount, rowsCountExpected)
	}

	return tb, strg
}

var createdBenchTables = make(map[string]bool)

func createBenchTable(b *testing.B, path string, startTimestamp int64, rowsPerInsert, rowsCount, tsidsCount int) {
	b.Helper()

	strg := newTestStorage()
	tb := mustOpenTable(path, strg)

	var insertsCount atomic.Int64
	insertsCount.Store(int64((rowsCount + rowsPerInsert - 1) / rowsPerInsert))

	var timestamp atomic.Uint64
	timestamp.Store(uint64(startTimestamp))

	var wg sync.WaitGroup
	for n := range cgroup.AvailableCPUs() {
		wg.Go(func() {
			rng := rand.New(rand.NewSource(int64(n)))
			rows := make([]rawRow, rowsPerInsert)
			value := float64(100)
			for insertsCount.Add(-1) >= 0 {
				for j := range rowsPerInsert {
					ts := timestamp.Add(uint64(10 + rng.Int63n(2)))
					value += float64(int(rng.NormFloat64() * 5))

					r := &rows[j]
					r.PrecisionBits = defaultPrecisionBits
					r.TSID.MetricID = uint64(rng.Intn(tsidsCount) + 1)
					r.Timestamp = int64(ts)
					r.Value = value
				}
				tb.MustAddRows(rows)
			}
		})
	}
	wg.Wait()

	tb.MustClose()
	stopTestStorage(strg)
}

func benchmarkTableSearch(b *testing.B, rowsCount, tsidsCount, tsidsSearch int) {
	startTimestamp := timestampFromTime(time.Now()) - 365*24*3600*1000
	rowsPerInsert := maxRawRowsPerShard

	tb, strg := openBenchTable(b, startTimestamp, rowsPerInsert, rowsCount, tsidsCount)
	tr := TimeRange{
		MinTimestamp: startTimestamp,
		MaxTimestamp: (1 << 63) - 1,
	}

	b.ResetTimer()
	b.ReportAllocs()
	rowsPerBench := min(int64(float64(rowsCount)*float64(tsidsSearch)/float64(tsidsCount)), int64(rowsCount))
	b.SetBytes(rowsPerBench)
	b.RunParallel(func(pb *testing.PB) {
		var ts tableSearch
		tsids := make([]TSID, tsidsSearch)
		var tmpBlock Block
		for pb.Next() {
			for i := range tsids {
				tsids[i].MetricID = 1 + uint64(i)
			}
			ts.Init(tb, tsids, tr)
			for ts.NextBlock() {
				ts.BlockRef.MustReadBlock(&tmpBlock)
			}
			ts.MustClose()
		}
	})
	b.StopTimer()

	tb.MustClose()
	stopTestStorage(strg)
}
