// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package dense_test

import (
	"iter"
	"math/rand"
	"slices"
	"testing"
	"time"

	"honnef.co/go/tools/analysis/dfa/dense"
)

type simpleGraph struct {
	numNodes int
	edges    [][]int
}

func (g *simpleGraph) NumNodes() int {
	return g.numNodes
}

func (g *simpleGraph) Nodes() iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := range g.numNodes {
			if !yield(i) {
				break
			}
		}
	}
}

func (g *simpleGraph) Out(nid int) iter.Seq[int] {
	return slices.Values(g.edges[nid])
}

func checkAnalysis[Fact comparable](t *testing.T, analysis *dense.Analysis[Fact, int], wantIns map[int]Fact, wantEdges map[[2]int]Fact) {
	t.Helper()
	for nid, want := range wantIns {
		if got := analysis.In(nid); got != want {
			t.Errorf("In(%d): got %v, want %v", nid, got, want)
		}
	}

	for edge, want := range wantEdges {
		from, to := edge[0], edge[1]
		if got := analysis.Edge(from, to); got != want {
			t.Errorf("Edge(%d, %d): got %v, want %v", from, to, got, want)
		}
	}
}

func TestBasic(t *testing.T) {
	// Define a trivial graph of 4 nodes in sequence: 0 -> 1 -> 2 -> 3
	g := &simpleGraph{
		numNodes: 4,
		edges: [][]int{
			0: {1},
			1: {2},
			2: {3},
			3: {},
		},
	}

	// Transfer function: OR source node's ID into the bitset.
	transfer := func(from, to int, fact nodeSet) nodeSet {
		return fact | (1 << uint(from))
	}

	analysis := dense.Forward[nodeSetUnion](g, nil, transfer)

	checkAnalysis(t, analysis,
		map[int]nodeSet{
			0: 0,
			1: set(0),
			2: set(0, 1),
			3: set(0, 1, 2),
		},
		map[[2]int]nodeSet{
			{0, 1}: set(0),
			{1, 2}: set(0, 1),
			{2, 3}: set(0, 1, 2),
		},
	)
}

func TestDiamond(t *testing.T) {
	// Diamond graph:
	//   0
	//  / \
	// 1   2
	//  \ /
	//   3
	g := &simpleGraph{
		numNodes: 4,
		edges: [][]int{
			0: {1, 2},
			1: {3},
			2: {3},
			3: {},
		},
	}

	transfer := func(from, to int, fact nodeSet) nodeSet {
		return fact | (1 << uint(from))
	}

	analysis := dense.Forward[nodeSetUnion](g, nil, transfer)

	checkAnalysis(t, analysis,
		map[int]nodeSet{
			0: 0,
			1: set(0), // Edge (0, 1) carries NodeID 0
			2: set(0), // Edge (0, 2) carries NodeID 0
			3: set(0, 1, 2),
		},
		map[[2]int]nodeSet{
			{0, 1}: set(0),
			{0, 2}: set(0),
			{1, 3}: set(0, 1),
			{2, 3}: set(0, 2),
		},
	)
}

func TestCycle(t *testing.T) {
	// Graph with a cycle:
	// 0 -> 1 -> 2
	//      ^    |
	//      +----+
	g := &simpleGraph{
		numNodes: 3,
		edges: [][]int{
			0: {1},
			1: {2},
			2: {1},
		},
	}

	transfer := func(from, to int, fact nodeSet) nodeSet {
		return fact | (1 << uint(from))
	}

	analysis := dense.Forward[nodeSetUnion](g, nil, transfer)

	checkAnalysis(t, analysis,
		map[int]nodeSet{
			0: 0,
			1: set(0, 1, 2),
			2: set(0, 1, 2),
		},
		map[[2]int]nodeSet{
			{0, 1}: set(0),
			{1, 2}: set(0, 1, 2),
			{2, 1}: set(0, 1, 2),
		},
	)
}

func TestFlowOrder(t *testing.T) {
	// This test constructs a "Spine with Bottleneck" graph in which a naive
	// visit order would result in quadratic time.
	//
	// Structure:
	//  - Spine: Chain 0 -> 1 -> ... -> T-1.
	//  - Bottleneck: All spine nodes (0..T-1) also point to T.
	//  - Tail: T begins a chain T -> T+1 -> ... -> N-1.
	//
	// However, we randomly permute the node IDs to prevent a "lucky" order
	// derived from the node IDs.
	//
	// With a naive order, updates to T from the spine would arrive one by one
	// (or in small batches), triggering re-evaluation of the entire tail
	// multiple times, and O(T*N) transfer calls.
	//
	// With RPO, T is evaluated once after all spine nodes are processed.

	T := 64 // Max ID allowed by nodeSet
	N := 2 * T

	// Permute IDs to prevent "lucky" ordering.
	perm := rand.New(rand.NewSource(42)).Perm(N)
	invPerm := make([]int, N)
	for i, p := range perm {
		invPerm[p] = i
	}
	// node maps a logical node ID to a permuted NodeID.
	node := func(logical int) int {
		return int(perm[logical])
	}

	// Build the edges.
	edges := make([][]int, N)
	nEdges := 0
	addEdge := func(u, v int) {
		uID := node(u)
		edges[uID] = append(edges[uID], node(v))
		nEdges++
	}
	// Spine: 0 -> 1 -> ... -> T-1
	for i := range T {
		if i+1 != T {
			addEdge(i, i+1)
		}
		addEdge(i, T)
	}
	// Tail: T -> T+1 -> ... -> N-1
	for i := T; i < N-2; i++ {
		addEdge(i, i+1)
	}

	g := &simpleGraph{
		numNodes: N,
		edges:    edges,
	}

	transfers := 0
	// Transfer: if pred is on the spine (< T), set its bit.
	transfer := func(from, to int, in nodeSet) nodeSet {
		transfers++
		logicalPred := invPerm[from]
		if logicalPred < T {
			return in | (1 << uint64(logicalPred))
		}
		return in
	}

	// Measure dense.Forward (RPO)
	start := time.Now()
	_ = dense.Forward[nodeSetUnion](g, nil, transfer)
	durRPO := time.Since(start)
	t.Logf("RPO time: %v", durRPO)

	// RPO should result in optimal evaluation, visiting each edge exactly once.
	if transfers != nEdges {
		t.Errorf("got %d transfer calls, expected %d for optimal evaluation strategy", transfers, nEdges)
	}
}
