// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package graph

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gonum.org/v1/gonum/graph/simple"

	"go.opentelemetry.io/collector/component"
	"go.opentelemetry.io/collector/component/componentstatus"
	"go.opentelemetry.io/collector/component/componenttest"
	"go.opentelemetry.io/collector/connector"
	"go.opentelemetry.io/collector/connector/connectortest"
	"go.opentelemetry.io/collector/exporter"
	"go.opentelemetry.io/collector/exporter/exportertest"
	"go.opentelemetry.io/collector/pipeline"
	"go.opentelemetry.io/collector/processor"
	"go.opentelemetry.io/collector/processor/processortest"
	"go.opentelemetry.io/collector/receiver"
	"go.opentelemetry.io/collector/receiver/receivertest"
	"go.opentelemetry.io/collector/service/internal/builders"
	"go.opentelemetry.io/collector/service/internal/status"
	"go.opentelemetry.io/collector/service/internal/status/statustest"
	"go.opentelemetry.io/collector/service/pipelines"
)

func TestGraphStartStop(t *testing.T) {
	t.Run("with_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, true)
		testGraphStartStop(t)
	})
	t.Run("without_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, false)
		testGraphStartStop(t)
	})
}

func testGraphStartStop(t *testing.T) {
	testCases := []struct {
		name  string
		edges [][2]component.ID
	}{
		{
			name: "single",
			edges: [][2]component.ID{
				{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "1")},
				{component.MustNewIDWithName("r", "2"), component.MustNewIDWithName("p", "1")},
				{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("p", "2")},
				{component.MustNewIDWithName("p", "2"), component.MustNewIDWithName("e", "1")},
				{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("e", "2")},
			},
		},
		{
			name: "multi",
			edges: [][2]component.ID{
				// Pipeline 1
				{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "1")},
				{component.MustNewIDWithName("r", "2"), component.MustNewIDWithName("p", "1")},
				{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("p", "2")},
				{component.MustNewIDWithName("p", "2"), component.MustNewIDWithName("e", "1")},
				{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("e", "2")},

				// Pipeline 2, shares r1 and e2
				{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "3")},
				{component.MustNewIDWithName("p", "3"), component.MustNewIDWithName("e", "2")},
			},
		},
		{
			name: "connected",
			edges: [][2]component.ID{
				// Pipeline 1
				{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "1")},
				{component.MustNewIDWithName("r", "2"), component.MustNewIDWithName("p", "1")},
				{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("p", "2")},
				{component.MustNewIDWithName("p", "2"), component.MustNewIDWithName("e", "1")},
				{component.MustNewIDWithName("p", "1"), component.MustNewIDWithName("c", "1")},

				// Pipeline 2, shares r1 and c1
				{component.MustNewIDWithName("r", "1"), component.MustNewIDWithName("p", "3")},
				{component.MustNewIDWithName("p", "3"), component.MustNewIDWithName("c", "1")},

				// Pipeline 3, emits to e2 and c2
				{component.MustNewIDWithName("c", "1"), component.MustNewIDWithName("e", "2")},
				{component.MustNewIDWithName("c", "1"), component.MustNewIDWithName("c", "2")},

				// Pipeline 4, also emits to e2
				{component.MustNewIDWithName("c", "2"), component.MustNewIDWithName("e", "2")},
			},
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			ctx := &contextWithOrder{
				Context: context.Background(),
				order:   map[component.ID]int{},
			}

			pg := &Graph{componentGraph: simple.NewDirectedGraph()}
			pg.telemetry = componenttest.NewNopTelemetrySettings()
			pg.instanceIDs = make(map[int64]*componentstatus.InstanceID)

			for _, edge := range tt.edges {
				f, t := &testNode{id: edge[0]}, &testNode{id: edge[1]}
				pg.instanceIDs[f.ID()] = &componentstatus.InstanceID{}
				pg.instanceIDs[t.ID()] = &componentstatus.InstanceID{}
				pg.componentGraph.SetEdge(simple.Edge{F: f, T: t})
			}

			require.NoError(t, pg.StartAll(ctx, &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
			for _, edge := range tt.edges {
				assert.Greater(t, ctx.order[edge[0]], ctx.order[edge[1]])
			}

			ctx.order = map[component.ID]int{}
			require.NoError(t, pg.ShutdownAll(ctx, statustest.NewNopStatusReporter()))
			for _, edge := range tt.edges {
				assert.Less(t, ctx.order[edge[0]], ctx.order[edge[1]])
			}
		})
	}
}

func TestGraphStartStopCycle(t *testing.T) {
	t.Run("with_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, true)
		testGraphStartStopCycle(t)
	})
	t.Run("without_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, false)
		testGraphStartStopCycle(t)
	})
}

func testGraphStartStopCycle(t *testing.T) {
	pg := &Graph{componentGraph: simple.NewDirectedGraph()}

	r1 := &testNode{id: component.MustNewIDWithName("r", "1")}
	p1 := &testNode{id: component.MustNewIDWithName("p", "1")}
	c1 := &testNode{id: component.MustNewIDWithName("c", "1")}
	e1 := &testNode{id: component.MustNewIDWithName("e", "1")}

	pg.instanceIDs = map[int64]*componentstatus.InstanceID{
		r1.ID(): {},
		p1.ID(): {},
		c1.ID(): {},
		e1.ID(): {},
	}

	pg.componentGraph.SetEdge(simple.Edge{F: r1, T: p1})
	pg.componentGraph.SetEdge(simple.Edge{F: p1, T: c1})
	pg.componentGraph.SetEdge(simple.Edge{F: c1, T: e1})
	pg.componentGraph.SetEdge(simple.Edge{F: c1, T: p1}) // loop back

	err := pg.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})})
	require.ErrorContains(t, err, `topo: no topological ordering: cyclic components`)

	err = pg.ShutdownAll(context.Background(), statustest.NewNopStatusReporter())
	assert.ErrorContains(t, err, `topo: no topological ordering: cyclic components`)
}

func TestGraphStartStopComponentError(t *testing.T) {
	t.Run("with_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, true)
		testGraphStartStopComponentError(t)
	})
	t.Run("without_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, false)
		testGraphStartStopComponentError(t)
	})
}

func testGraphStartStopComponentError(t *testing.T) {
	pg := &Graph{componentGraph: simple.NewDirectedGraph()}
	pg.telemetry = componenttest.NewNopTelemetrySettings()
	r1 := &testNode{
		id:       component.MustNewIDWithName("r", "1"),
		startErr: errors.New("foo"),
	}
	e1 := &testNode{
		id:          component.MustNewIDWithName("e", "1"),
		shutdownErr: errors.New("bar"),
	}
	pg.instanceIDs = map[int64]*componentstatus.InstanceID{
		r1.ID(): {},
		e1.ID(): {},
	}
	pg.componentGraph.SetEdge(simple.Edge{
		F: r1,
		T: e1,
	})
	require.ErrorIs(t, pg.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}), r1.startErr)
	assert.EqualError(t, pg.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()), "bar")
}

// This includes all tests from the previous implementation, plus a new one
// relevant only to the new graph-based implementation.
func TestGraphFailToStartAndShutdown(t *testing.T) {
	t.Run("with_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, true)
		testGraphFailToStartAndShutdown(t)
	})
	t.Run("without_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, false)
		testGraphFailToStartAndShutdown(t)
	})
}

func testGraphFailToStartAndShutdown(t *testing.T) {
	errReceiverFactory := newErrReceiverFactory()
	errProcessorFactory := newErrProcessorFactory()
	errExporterFactory := newErrExporterFactory()
	errConnectorFactory := newErrConnectorFactory()
	nopReceiverFactory := receivertest.NewNopFactory()
	nopProcessorFactory := processortest.NewNopFactory()
	nopExporterFactory := exportertest.NewNopFactory()
	nopConnectorFactory := connectortest.NewNopFactory()

	set := Settings{
		Telemetry: componenttest.NewNopTelemetrySettings(),
		BuildInfo: component.NewDefaultBuildInfo(),
		ReceiverBuilder: builders.NewReceiver(
			map[component.ID]component.Config{
				component.NewID(nopReceiverFactory.Type()): nopReceiverFactory.CreateDefaultConfig(),
				component.NewID(errReceiverFactory.Type()): errReceiverFactory.CreateDefaultConfig(),
			},
			map[component.Type]receiver.Factory{
				nopReceiverFactory.Type(): nopReceiverFactory,
				errReceiverFactory.Type(): errReceiverFactory,
			}),
		ProcessorBuilder: builders.NewProcessor(
			map[component.ID]component.Config{
				component.NewID(nopProcessorFactory.Type()): nopProcessorFactory.CreateDefaultConfig(),
				component.NewID(errProcessorFactory.Type()): errProcessorFactory.CreateDefaultConfig(),
			},
			map[component.Type]processor.Factory{
				nopProcessorFactory.Type(): nopProcessorFactory,
				errProcessorFactory.Type(): errProcessorFactory,
			}),
		ExporterBuilder: builders.NewExporter(
			map[component.ID]component.Config{
				component.NewID(nopExporterFactory.Type()): nopExporterFactory.CreateDefaultConfig(),
				component.NewID(errExporterFactory.Type()): errExporterFactory.CreateDefaultConfig(),
			},
			map[component.Type]exporter.Factory{
				nopExporterFactory.Type(): nopExporterFactory,
				errExporterFactory.Type(): errExporterFactory,
			}),
		ConnectorBuilder: builders.NewConnector(
			map[component.ID]component.Config{
				component.NewIDWithName(nopConnectorFactory.Type(), "conn"): nopConnectorFactory.CreateDefaultConfig(),
				component.NewIDWithName(errConnectorFactory.Type(), "conn"): errConnectorFactory.CreateDefaultConfig(),
			},
			map[component.Type]connector.Factory{
				nopConnectorFactory.Type(): nopConnectorFactory,
				errConnectorFactory.Type(): errConnectorFactory,
			}),
	}

	dataTypes := []pipeline.Signal{pipeline.SignalTraces, pipeline.SignalMetrics, pipeline.SignalLogs}
	for _, dt := range dataTypes {
		t.Run(dt.String()+"/receiver", func(t *testing.T) {
			set.PipelineConfigs = pipelines.Config{
				pipeline.NewID(dt): {
					Receivers:  []component.ID{component.MustNewID("nop"), component.MustNewID("err")},
					Processors: []component.ID{component.MustNewID("nop")},
					Exporters:  []component.ID{component.MustNewID("nop")},
				},
			}
			pipelines, err := Build(context.Background(), set)
			require.NoError(t, err)
			require.Error(t, pipelines.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
			assert.Error(t, pipelines.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()))
		})

		t.Run(dt.String()+"/processor", func(t *testing.T) {
			set.PipelineConfigs = pipelines.Config{
				pipeline.NewID(dt): {
					Receivers:  []component.ID{component.MustNewID("nop")},
					Processors: []component.ID{component.MustNewID("nop"), component.MustNewID("err")},
					Exporters:  []component.ID{component.MustNewID("nop")},
				},
			}
			pipelines, err := Build(context.Background(), set)
			require.NoError(t, err)
			require.Error(t, pipelines.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
			assert.Error(t, pipelines.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()))
		})

		t.Run(dt.String()+"/exporter", func(t *testing.T) {
			set.PipelineConfigs = pipelines.Config{
				pipeline.NewID(dt): {
					Receivers:  []component.ID{component.MustNewID("nop")},
					Processors: []component.ID{component.MustNewID("nop")},
					Exporters:  []component.ID{component.MustNewID("nop"), component.MustNewID("err")},
				},
			}
			pipelines, err := Build(context.Background(), set)
			require.NoError(t, err)
			require.Error(t, pipelines.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
			assert.Error(t, pipelines.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()))
		})

		for _, dt2 := range dataTypes {
			t.Run(dt.String()+"/"+dt2.String()+"/connector", func(t *testing.T) {
				set.PipelineConfigs = pipelines.Config{
					pipeline.NewIDWithName(dt, "in"): {
						Receivers:  []component.ID{component.MustNewID("nop")},
						Processors: []component.ID{component.MustNewID("nop")},
						Exporters:  []component.ID{component.MustNewID("nop"), component.MustNewIDWithName("err", "conn")},
					},
					pipeline.NewIDWithName(dt2, "out"): {
						Receivers:  []component.ID{component.MustNewID("nop"), component.MustNewIDWithName("err", "conn")},
						Processors: []component.ID{component.MustNewID("nop")},
						Exporters:  []component.ID{component.MustNewID("nop")},
					},
				}
				pipelines, err := Build(context.Background(), set)
				require.NoError(t, err)
				require.Error(t, pipelines.StartAll(context.Background(), &Host{Reporter: status.NewReporter(func(*componentstatus.InstanceID, *componentstatus.Event) {}, func(error) {})}))
				assert.Error(t, pipelines.ShutdownAll(context.Background(), statustest.NewNopStatusReporter()))
			})
		}
	}
}

func TestStatusReportedOnStartupShutdown(t *testing.T) {
	t.Run("with_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, true)
		testStatusReportedOnStartupShutdown(t)
	})
	t.Run("without_internal_telemetry", func(t *testing.T) {
		setObsConsumerGateForTest(t, false)
		testStatusReportedOnStartupShutdown(t)
	})
}

func testStatusReportedOnStartupShutdown(t *testing.T) {
	rNoErr := &testNode{id: component.MustNewIDWithName("r_no_err", "1")}
	rStErr := &testNode{id: component.MustNewIDWithName("r_st_err", "1"), startErr: assert.AnError}
	rSdErr := &testNode{id: component.MustNewIDWithName("r_sd_err", "1"), shutdownErr: assert.AnError}

	eNoErr := &testNode{id: component.MustNewIDWithName("e_no_err", "1")}
	eStErr := &testNode{id: component.MustNewIDWithName("e_st_err", "1"), startErr: assert.AnError}
	eSdErr := &testNode{id: component.MustNewIDWithName("e_sd_err", "1"), shutdownErr: assert.AnError}

	instanceIDs := map[*testNode]*componentstatus.InstanceID{
		rNoErr: componentstatus.NewInstanceID(rNoErr.id, component.KindReceiver),
		rStErr: componentstatus.NewInstanceID(rStErr.id, component.KindReceiver),
		rSdErr: componentstatus.NewInstanceID(rSdErr.id, component.KindReceiver),
		eNoErr: componentstatus.NewInstanceID(eNoErr.id, component.KindExporter),
		eStErr: componentstatus.NewInstanceID(eStErr.id, component.KindExporter),
		eSdErr: componentstatus.NewInstanceID(eSdErr.id, component.KindExporter),
	}

	// compare two maps of status events ignoring timestamp
	assertEqualStatuses := func(t *testing.T, evMap1, evMap2 map[*componentstatus.InstanceID][]*componentstatus.Event) {
		assert.Len(t, evMap2, len(evMap1))
		for id, evts1 := range evMap1 {
			evts2 := evMap2[id]
			assert.Len(t, evts2, len(evts1))
			for i := 0; i < len(evts1); i++ {
				ev1 := evts1[i]
				ev2 := evts2[i]
				assert.Equal(t, ev1.Status(), ev2.Status())
				assert.Equal(t, ev1.Err(), ev2.Err())
			}
		}
	}

	for _, tt := range []struct {
		name             string
		edge             [2]*testNode
		expectedStatuses map[*componentstatus.InstanceID][]*componentstatus.Event
		startupErr       error
		shutdownErr      error
	}{
		{
			name: "successful startup/shutdown",
			edge: [2]*testNode{rNoErr, eNoErr},
			expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
				instanceIDs[rNoErr]: {
					componentstatus.NewEvent(componentstatus.StatusStarting),
					componentstatus.NewEvent(componentstatus.StatusOK),
					componentstatus.NewEvent(componentstatus.StatusStopping),
					componentstatus.NewEvent(componentstatus.StatusStopped),
				},
				instanceIDs[eNoErr]: {
					componentstatus.NewEvent(componentstatus.StatusStarting),
					componentstatus.NewEvent(componentstatus.StatusOK),
					componentstatus.NewEvent(componentstatus.StatusStopping),
					componentstatus.NewEvent(componentstatus.StatusStopped),
				},
			},
		},
		{
			name: "early startup error",
			edge: [2]*testNode{rNoErr, eStErr},
			expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
				instanceIDs[eStErr]: {
					componentstatus.NewEvent(componentstatus.StatusStarting),
					componentstatus.NewPermanentErrorEvent(assert.AnError),
					componentstatus.NewEvent(componentstatus.StatusStopping),
					componentstatus.NewEvent(componentstatus.StatusStopped),
				},
			},
			startupErr: assert.AnError,
		},
		{
			name: "late startup error",
			edge: [2]*testNode{rStErr, eNoErr},
			expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
				instanceIDs[rStErr]: {
					componentstatus.NewEvent(componentstatus.StatusStarting),
					componentstatus.NewPermanentErrorEvent(assert.AnError),
					componentstatus.NewEvent(componentstatus.StatusStopping),
					componentstatus.NewEvent(componentstatus.StatusStopped),
				},
				instanceIDs[eNoErr]: {
					componentstatus.NewEvent(componentstatus.StatusStarting),
					componentstatus.NewEvent(componentstatus.StatusOK),
					componentstatus.NewEvent(componentstatus.StatusStopping),
					componentstatus.NewEvent(componentstatus.StatusStopped),
				},
			},
			startupErr: assert.AnError,
		},
		{
			name: "early shutdown error",
			edge: [2]*testNode{rSdErr, eNoErr},
			expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
				instanceIDs[rSdErr]: {
					componentstatus.NewEvent(componentstatus.StatusStarting),
					componentstatus.NewEvent(componentstatus.StatusOK),
					componentstatus.NewEvent(componentstatus.StatusStopping),
					componentstatus.NewPermanentErrorEvent(assert.AnError),
				},
				instanceIDs[eNoErr]: {
					componentstatus.NewEvent(componentstatus.StatusStarting),
					componentstatus.NewEvent(componentstatus.StatusOK),
					componentstatus.NewEvent(componentstatus.StatusStopping),
					componentstatus.NewEvent(componentstatus.StatusStopped),
				},
			},
			shutdownErr: assert.AnError,
		},
		{
			name: "late shutdown error",
			edge: [2]*testNode{rNoErr, eSdErr},
			expectedStatuses: map[*componentstatus.InstanceID][]*componentstatus.Event{
				instanceIDs[rNoErr]: {
					componentstatus.NewEvent(componentstatus.StatusStarting),
					componentstatus.NewEvent(componentstatus.StatusOK),
					componentstatus.NewEvent(componentstatus.StatusStopping),
					componentstatus.NewEvent(componentstatus.StatusStopped),
				},
				instanceIDs[eSdErr]: {
					componentstatus.NewEvent(componentstatus.StatusStarting),
					componentstatus.NewEvent(componentstatus.StatusOK),
					componentstatus.NewEvent(componentstatus.StatusStopping),
					componentstatus.NewPermanentErrorEvent(assert.AnError),
				},
			},
			shutdownErr: assert.AnError,
		},
	} {
		t.Run(tt.name, func(t *testing.T) {
			pg := &Graph{componentGraph: simple.NewDirectedGraph()}
			pg.telemetry = componenttest.NewNopTelemetrySettings()

			actualStatuses := make(map[*componentstatus.InstanceID][]*componentstatus.Event)
			rep := status.NewReporter(func(id *componentstatus.InstanceID, ev *componentstatus.Event) {
				actualStatuses[id] = append(actualStatuses[id], ev)
			}, func(error) {
			})

			e0, e1 := tt.edge[0], tt.edge[1]
			pg.instanceIDs = map[int64]*componentstatus.InstanceID{
				e0.ID(): instanceIDs[e0],
				e1.ID(): instanceIDs[e1],
			}
			pg.componentGraph.SetEdge(simple.Edge{F: e0, T: e1})

			require.ErrorIs(t, pg.StartAll(context.Background(), &Host{Reporter: rep}), tt.startupErr)
			assert.Equal(t, tt.shutdownErr, pg.ShutdownAll(context.Background(), rep))
			assertEqualStatuses(t, tt.expectedStatuses, actualStatuses)
		})
	}
}
