package oonimkall

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	engine "github.com/ooni/probe-cli/v3/internal/engine"
	"github.com/ooni/probe-cli/v3/internal/model"
)

func TestMeasurementSubmissionEventName(t *testing.T) {
	if measurementSubmissionEventName(nil) != eventTypeStatusMeasurementSubmission {
		t.Fatal("unexpected submission event name")
	}
	if measurementSubmissionEventName(errors.New("mocked error")) != eventTypeFailureMeasurementSubmission {
		t.Fatal("unexpected submission event name")
	}
}

func TestMeasurementSubmissionFailure(t *testing.T) {
	if measurementSubmissionFailure(nil) != "" {
		t.Fatal("unexpected submission failure")
	}
	if measurementSubmissionFailure(errors.New("mocked error")) != "mocked error" {
		t.Fatal("unexpected submission failure")
	}
}

func TestTaskRunnerRun(t *testing.T) {
	if testing.Short() {
		t.Skip("skip test in short mode")
	}

	// newRunnerForTesting is a factory for creating a new
	// runner that wraps newRunner and also sets a specific
	// taskSessionBuilder for testing purposes.
	newRunnerForTesting := func() (*runnerForTask, *CollectorTaskEmitter) {
		settings := &settings{
			Name: "Example",
			Options: settingsOptions{
				SoftwareName:    "oonimkall-test",
				SoftwareVersion: "0.1.0",
			},
			StateDir: "testdata/state",
			Version:  1,
		}
		e := &CollectorTaskEmitter{}
		r := newRunner(settings, e)
		return r, e
	}

	// runAndCollectContext runs the task until completion
	// and collects the emitted events. Remember that
	// it's not race safe to modify the events.
	runAndCollectContext := func(ctx context.Context, r taskRunner, e *CollectorTaskEmitter) []*event {
		r.Run(ctx)
		return e.Collect()
	}

	// runAndCollect is like runAndCollectContext
	// but uses context.Background()
	runAndCollect := func(r taskRunner, e *CollectorTaskEmitter) []*event {
		return runAndCollectContext(context.Background(), r, e)
	}

	// countEventsByKey returns the number of events
	// with the given key inside of the list.
	countEventsByKey := func(events []*event, key string) (count int) {
		for _, ev := range events {
			if ev.Key == key {
				count++
			}
		}
		return
	}

	// assertCountEventsByKey fails is the number of events
	// of the given type is not the expected one.
	assertCountEventsByKey := func(events []*event, key string, count int) {
		if countEventsByKey(events, key) != count {
			t.Fatalf("unexpected number of '%s' events", key)
		}
	}

	t.Run("with unsupported settings", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		runner.settings.Version = 0 // force unsupported version
		events := runAndCollect(runner, emitter)
		assertCountEventsByKey(events, eventTypeFailureStartup, 1)
	})

	t.Run("with failure when creating a new kvstore", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		// override the kvstore builder to provoke an error
		runner.kvStoreBuilder = &MockableKVStoreFSBuilder{
			MockNewFS: func(path string) (model.KeyValueStore, error) {
				return nil, errors.New("generic error")
			},
		}
		events := runAndCollect(runner, emitter)
		assertCountEventsByKey(events, eventTypeFailureStartup, 1)
	})

	t.Run("with unparsable proxyURL", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		runner.settings.Proxy = "\t" // invalid proxy URL
		events := runAndCollect(runner, emitter)
		assertCountEventsByKey(events, eventTypeFailureStartup, 1)
	})

	t.Run("with a parsable proxyURL", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		// set a valid URL
		runner.settings.Proxy = "https://127.0.0.1/"
		// set a fake session builder that causes the startup to
		// fail but records the config passed to NewSession
		saver := &SessionBuilderConfigSaver{}
		runner.sessionBuilder = saver
		events := runAndCollect(runner, emitter)
		assertCountEventsByKey(events, eventTypeFailureStartup, 1)
		if saver.Config.ProxyURL.String() != runner.settings.Proxy {
			t.Fatal("invalid proxy URL")
		}
	})

	t.Run("with custom probe services URL", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		// set a probe services URL
		runner.settings.Options.ProbeServicesBaseURL = "https://127.0.0.1"
		// set a fake session builder that causes the startup to
		// fail but records the config passed to NewSession
		saver := &SessionBuilderConfigSaver{}
		runner.sessionBuilder = saver
		events := runAndCollect(runner, emitter)
		assertCountEventsByKey(events, eventTypeFailureStartup, 1)
		psu := saver.Config.AvailableProbeServices
		if len(psu) != 1 {
			t.Fatal("invalid length")
		}
		if psu[0].Type != "https" {
			t.Fatal("invalid type")
		}
		if psu[0].Address != runner.settings.Options.ProbeServicesBaseURL {
			t.Fatal("invalid address")
		}
		if psu[0].Front != "" {
			t.Fatal("invalid front")
		}
	})

	type eventKeyCount struct {
		Key   string
		Count int
	}

	// reduceEventsKeysIgnoreLog reduces the list of event keys
	// counting equal subsequent keys and ignoring log events
	reduceEventsKeysIgnoreLog := func(events []*event) (out []eventKeyCount) {
		var current eventKeyCount
		for _, ev := range events {
			if ev.Key == eventTypeLog {
				continue
			}
			if current.Key == ev.Key {
				current.Count++
				continue
			}
			if current.Key != "" {
				out = append(out, current)
			}
			current.Key = ev.Key
			current.Count = 1
		}
		if current.Key != "" {
			out = append(out, current)
		}
		return
	}

	// fakeSuccessfulRun returns a new set of dependencies that
	// will perform a fully successful, but fake, run.
	fakeSuccessfulRun := func() *MockableTaskRunnerDependencies {
		return &MockableTaskRunnerDependencies{
			MockableKibiBytesReceived: func() float64 {
				return 10
			},
			MockableKibiBytesSent: func() float64 {
				return 4
			},
			MockableOpenReportContext: func(ctx context.Context) error {
				return nil
			},
			MockableReportID: func() string {
				return "20211202T074907Z_example_IT_30722_n1_axDLHNUfJaV1IbuU"
			},
			MockableMeasureWithContext: func(ctx context.Context, input string) (*model.Measurement, error) {
				return &model.Measurement{}, nil
			},
			MockableSubmitAndUpdateMeasurementContext: func(ctx context.Context, measurement *model.Measurement) error {
				return nil
			},
			MockableSetCallbacks: func(callbacks model.ExperimentCallbacks) {
			},
			MockableInputPolicy: func() model.InputPolicy {
				return model.InputNone
			},
			MockableInterruptible: func() bool {
				return false
			},
			MockClose: func() error {
				return nil
			},
			MockMaybeLookupBackendsContext: func(ctx context.Context) error {
				return nil
			},
			MockMaybeLookupLocationContext: func(ctx context.Context) error {
				return nil
			},
			MockProbeIP: func() string {
				return "130.192.91.211"
			},
			MockProbeASNString: func() string {
				return "AS137"
			},
			MockProbeCC: func() string {
				return "IT"
			},
			MockProbeNetworkName: func() string {
				return "GARR"
			},
			MockResolverASNString: func() string {
				return "AS137"
			},
			MockResolverIP: func() string {
				return "130.192.3.24"
			},
			MockResolverNetworkName: func() string {
				return "GARR"
			},
		}
	}

	assertReducedEventsLike := func(t *testing.T, expected, got []eventKeyCount) {
		if diff := cmp.Diff(expected, got); diff != "" {
			t.Fatal(diff)
		}
	}

	t.Run("with invalid experiment name", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockNewExperimentBuilderByName = func(name string) (taskExperimentBuilder, error) {
			return nil, errors.New("invalid experiment name")
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeFailureStartup, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with error during backends lookup", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockMaybeLookupBackendsContext = func(ctx context.Context) error {
			return errors.New("mocked error")
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeFailureStartup, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with error during location lookup", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockMaybeLookupLocationContext = func(ctx context.Context) error {
			return errors.New("mocked error")
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeFailureIPLookup, Count: 1},
			{Key: eventTypeFailureASNLookup, Count: 1},
			{Key: eventTypeFailureCCLookup, Count: 1},
			{Key: eventTypeFailureResolverLookup, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with missing input and InputOrQueryBackend policy", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputOrQueryBackend
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeFailureStartup, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with missing input and InputStrictlyRequired policy", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputStrictlyRequired
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeFailureStartup, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run(
		"with InputOrStaticDefault policy and experiment with no static input",
		func(t *testing.T) {
			runner, emitter := newRunnerForTesting()
			runner.settings.Name = "Antani" // no input for this experiment
			fake := fakeSuccessfulRun()
			fake.MockableInputPolicy = func() model.InputPolicy {
				return model.InputOrStaticDefault
			}
			runner.sessionBuilder = fake
			events := runAndCollect(runner, emitter)
			reduced := reduceEventsKeysIgnoreLog(events)
			expect := []eventKeyCount{
				{Key: eventTypeStatusQueued, Count: 1},
				{Key: eventTypeStatusStarted, Count: 1},
				{Key: eventTypeStatusProgress, Count: 3},
				{Key: eventTypeStatusGeoIPLookup, Count: 1},
				{Key: eventTypeStatusResolverLookup, Count: 1},
				{Key: eventTypeFailureStartup, Count: 1},
				{Key: eventTypeStatusEnd, Count: 1},
			}
			assertReducedEventsLike(t, expect, reduced)
		})

	t.Run("with InputNone policy and provided input", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		runner.settings.Inputs = append(runner.settings.Inputs, "https://x.org/")
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputNone
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeFailureStartup, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with failure opening report", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockableOpenReportContext = func(ctx context.Context) error {
			return errors.New("mocked error")
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeFailureReportCreate, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with success and InputNone policy", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputNone
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with measurement failure and InputNone policy", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputNone
		}
		fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
			return nil, errors.New("preconditions error")
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeFailureMeasurement, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with measurement failure and annotations", func(t *testing.T) {
		// See https://github.com/ooni/probe/issues/2173. We want to be sure that
		// we are not crashing when the measurement fails and there are annotations,
		// which is what was happening in the above referenced issue.
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputNone
		}
		fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
			return nil, errors.New("preconditions error")
		}
		runner.sessionBuilder = fake
		runner.settings.Annotations = map[string]string{
			"architecture": "arm64",
		}
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeFailureMeasurement, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with success and InputStrictlyRequired", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		runner.settings.Inputs = []string{"a", "b", "c", "d"}
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputStrictlyRequired
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with success and InputOptional and input", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		runner.settings.Inputs = []string{"a", "b", "c", "d"}
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputOptional
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with success and InputOptional and no input", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputOptional
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with success and InputOrStaticDefault", func(t *testing.T) {
		experimentName := "DNSCheck"
		runner, emitter := newRunnerForTesting()
		runner.settings.Name = experimentName
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputOrStaticDefault
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
		}
		allEntries, err := engine.StaticBareInputForExperiment(experimentName)
		if err != nil {
			t.Fatal(err)
		}
		// write the correct entries for each expected measurement.
		for idx := 0; idx < len(allEntries); idx++ {
			expect = append(expect, eventKeyCount{Key: eventTypeStatusMeasurementStart, Count: 1})
			expect = append(expect, eventKeyCount{Key: eventTypeStatusProgress, Count: 1})
			expect = append(expect, eventKeyCount{Key: eventTypeMeasurement, Count: 1})
			expect = append(expect, eventKeyCount{Key: eventTypeStatusMeasurementSubmission, Count: 1})
			expect = append(expect, eventKeyCount{Key: eventTypeStatusMeasurementDone, Count: 1})
		}
		expect = append(expect, eventKeyCount{Key: eventTypeStatusEnd, Count: 1})
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with succes and max runtime", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		runner.settings.Inputs = []string{"a", "b", "c", "d"}
		runner.settings.Options.MaxRuntime = 2
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputStrictlyRequired
		}
		fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
			time.Sleep(1 * time.Second)
			return &model.Measurement{}, nil
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with interrupted experiment", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		runner.settings.Inputs = []string{"a", "b", "c", "d"}
		runner.settings.Options.MaxRuntime = 2
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputStrictlyRequired
		}
		fake.MockableInterruptible = func() bool {
			return true
		}
		ctx, cancel := context.WithCancel(context.Background())
		fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
			cancel()
			return &model.Measurement{}, nil
		}
		runner.sessionBuilder = fake
		events := runAndCollectContext(ctx, runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			//
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with measurement submission failure", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		runner.settings.Inputs = []string{"a"}
		fake := fakeSuccessfulRun()
		fake.MockableInputPolicy = func() model.InputPolicy {
			return model.InputStrictlyRequired
		}
		fake.MockableSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error {
			return errors.New("cannot submit")
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			//
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeFailureMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			//
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})

	t.Run("with success and progress", func(t *testing.T) {
		runner, emitter := newRunnerForTesting()
		fake := fakeSuccessfulRun()
		var callbacks model.ExperimentCallbacks
		fake.MockableSetCallbacks = func(cbs model.ExperimentCallbacks) {
			callbacks = cbs
		}
		fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
			callbacks.OnProgress(1, "hello from the fake experiment")
			return &model.Measurement{}, nil
		}
		runner.sessionBuilder = fake
		events := runAndCollect(runner, emitter)
		reduced := reduceEventsKeysIgnoreLog(events)
		expect := []eventKeyCount{
			{Key: eventTypeStatusQueued, Count: 1},
			{Key: eventTypeStatusStarted, Count: 1},
			{Key: eventTypeStatusProgress, Count: 3},
			{Key: eventTypeStatusGeoIPLookup, Count: 1},
			{Key: eventTypeStatusResolverLookup, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeStatusReportCreate, Count: 1},
			{Key: eventTypeStatusMeasurementStart, Count: 1},
			{Key: eventTypeStatusProgress, Count: 1},
			{Key: eventTypeMeasurement, Count: 1},
			{Key: eventTypeStatusMeasurementSubmission, Count: 1},
			{Key: eventTypeStatusMeasurementDone, Count: 1},
			{Key: eventTypeStatusEnd, Count: 1},
		}
		assertReducedEventsLike(t, expect, reduced)
	})
}
