package engine

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

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

type FakeInputProcessorExperiment struct {
	SleepTime time.Duration
	Err       error
	M         []*model.Measurement
}

func (fipe *FakeInputProcessorExperiment) MeasureAsync(
	ctx context.Context, input string) (<-chan *model.Measurement, error) {
	if fipe.Err != nil {
		return nil, fipe.Err
	}
	if fipe.SleepTime > 0 {
		time.Sleep(fipe.SleepTime)
	}
	m := new(model.Measurement)
	// Here we add annotations to ensure that the input processor
	// is MERGING annotations as opposed to overwriting them.
	m.AddAnnotation("antani", "antani")
	m.AddAnnotation("foo", "baz") // would be bar below
	m.Input = model.MeasurementTarget(input)
	fipe.M = append(fipe.M, m)
	out := make(chan *model.Measurement)
	go func() {
		defer close(out)
		out <- m
	}()
	return out, nil
}

func TestInputProcessorMeasurementFailed(t *testing.T) {
	expected := errors.New("mocked error")
	ip := &InputProcessor{
		Experiment: NewInputProcessorExperimentWrapper(
			&FakeInputProcessorExperiment{Err: expected},
		),
		Inputs: []model.URLInfo{{
			URL: "https://www.kernel.org/",
		}},
	}
	ctx := context.Background()
	if err := ip.Run(ctx); !errors.Is(err, expected) {
		t.Fatalf("not the error we expected: %+v", err)
	}
}

type FakeInputProcessorSubmitter struct {
	Err error
	M   []*model.Measurement
}

func (fips *FakeInputProcessorSubmitter) Submit(
	ctx context.Context, m *model.Measurement) error {
	fips.M = append(fips.M, m)
	return fips.Err
}

func TestInputProcessorSubmissionFailed(t *testing.T) {
	fipe := &FakeInputProcessorExperiment{}
	expected := errors.New("mocked error")
	ip := &InputProcessor{
		Annotations: map[string]string{
			"foo": "bar",
		},
		Experiment: NewInputProcessorExperimentWrapper(fipe),
		Inputs: []model.URLInfo{{
			URL: "https://www.kernel.org/",
		}},
		Options: []string{"fake=true"},
		Submitter: NewInputProcessorSubmitterWrapper(
			&FakeInputProcessorSubmitter{Err: expected},
		),
	}
	ctx := context.Background()
	if err := ip.Run(ctx); !errors.Is(err, expected) {
		t.Fatalf("not the error we expected: %+v", err)
	}
	if len(fipe.M) != 1 {
		t.Fatal("no measurements generated")
	}
	m := fipe.M[0]
	if m.Input != "https://www.kernel.org/" {
		t.Fatal("invalid input")
	}
	if len(m.Annotations) != 2 {
		t.Fatal("invalid number of annotations")
	}
	if m.Annotations["foo"] != "bar" {
		t.Fatal("invalid annotation: foo")
	}
	if m.Annotations["antani"] != "antani" {
		t.Fatal("invalid annotation: antani")
	}
	if len(m.Options) != 1 || m.Options[0] != "fake=true" {
		t.Fatal("options not set")
	}
}

type FakeInputProcessorSaver struct {
	Err error
	M   []*model.Measurement
}

func (fips *FakeInputProcessorSaver) SaveMeasurement(m *model.Measurement) error {
	fips.M = append(fips.M, m)
	return fips.Err
}

func TestInputProcessorSaveOnDiskFailed(t *testing.T) {
	expected := errors.New("mocked error")
	ip := &InputProcessor{
		Experiment: NewInputProcessorExperimentWrapper(
			&FakeInputProcessorExperiment{},
		),
		Inputs: []model.URLInfo{{
			URL: "https://www.kernel.org/",
		}},
		Options: []string{"fake=true"},
		Saver: NewInputProcessorSaverWrapper(
			&FakeInputProcessorSaver{Err: expected},
		),
		Submitter: NewInputProcessorSubmitterWrapper(
			&FakeInputProcessorSubmitter{Err: nil},
		),
	}
	ctx := context.Background()
	if err := ip.Run(ctx); !errors.Is(err, expected) {
		t.Fatalf("not the error we expected: %+v", err)
	}
}

func TestInputProcessorGood(t *testing.T) {
	fipe := &FakeInputProcessorExperiment{}
	saver := &FakeInputProcessorSaver{Err: nil}
	submitter := &FakeInputProcessorSubmitter{Err: nil}
	ip := &InputProcessor{
		Experiment: NewInputProcessorExperimentWrapper(fipe),
		Inputs: []model.URLInfo{{
			URL: "https://www.kernel.org/",
		}, {
			URL: "https://www.slashdot.org/",
		}},
		Options:   []string{"fake=true"},
		Saver:     NewInputProcessorSaverWrapper(saver),
		Submitter: NewInputProcessorSubmitterWrapper(submitter),
	}
	ctx := context.Background()
	reason, err := ip.run(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if reason != stopNormal {
		t.Fatal("terminated by max runtime!?")
	}
	if len(fipe.M) != 2 || len(saver.M) != 2 || len(submitter.M) != 2 {
		t.Fatal("not all measurements saved")
	}
	if submitter.M[0].Input != "https://www.kernel.org/" {
		t.Fatal("invalid submitter.M[0].Input")
	}
	if submitter.M[1].Input != "https://www.slashdot.org/" {
		t.Fatal("invalid submitter.M[1].Input")
	}
	if saver.M[0].Input != "https://www.kernel.org/" {
		t.Fatal("invalid saver.M[0].Input")
	}
	if saver.M[1].Input != "https://www.slashdot.org/" {
		t.Fatal("invalid saver.M[1].Input")
	}
}

func TestInputProcessorMaxRuntime(t *testing.T) {
	fipe := &FakeInputProcessorExperiment{
		SleepTime: 50 * time.Millisecond,
	}
	saver := &FakeInputProcessorSaver{Err: nil}
	submitter := &FakeInputProcessorSubmitter{Err: nil}
	ip := &InputProcessor{
		Experiment: NewInputProcessorExperimentWrapper(fipe),
		Inputs: []model.URLInfo{{
			URL: "https://www.kernel.org/",
		}, {
			URL: "https://www.slashdot.org/",
		}},
		MaxRuntime: 1 * time.Nanosecond,
		Options:    []string{"fake=true"},
		Saver:      NewInputProcessorSaverWrapper(saver),
		Submitter:  NewInputProcessorSubmitterWrapper(submitter),
	}
	ctx := context.Background()
	reason, err := ip.run(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if reason != stopMaxRuntime {
		t.Fatal("not terminated by max runtime")
	}
}