// Copyright (C) 2023 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

package model

import (
	"context"
	"strings"
	"testing"

	"github.com/syncthing/syncthing/lib/events"
	"github.com/thejerf/suture/v4"
)

func TestServiceMap(t *testing.T) {
	t.Parallel()
	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	sup := suture.NewSimple("TestServiceMap")
	sup.ServeBackground(ctx)

	t.Run("SimpleAddRemove", func(t *testing.T) {
		t.Parallel()

		sm := newServiceMap[string, *dummyService](events.NoopLogger)
		sup.Add(sm)

		// Add two services. They should start.

		d1 := newDummyService()
		d2 := newDummyService()

		sm.Add("d1", d1)
		sm.Add("d2", d2)

		<-d1.started
		<-d2.started

		// Remove them. They should stop.

		if !sm.Remove("d1") {
			t.Errorf("Remove failed")
		}
		if !sm.Remove("d2") {
			t.Errorf("Remove failed")
		}

		<-d1.stopped
		<-d2.stopped
	})

	t.Run("OverwriteImpliesRemove", func(t *testing.T) {
		t.Parallel()

		sm := newServiceMap[string, *dummyService](events.NoopLogger)
		sup.Add(sm)

		d1 := newDummyService()
		d2 := newDummyService()

		// Add d1, it should start.

		sm.Add("k", d1)
		<-d1.started

		// Add d2, with the same key. The previous one should stop as we're
		// doing a replace.

		sm.Add("k", d2)
		<-d1.stopped
		<-d2.started

		if !sm.Remove("k") {
			t.Errorf("Remove failed")
		}

		<-d2.stopped
	})

	t.Run("IterateWithRemoveAndWait", func(t *testing.T) {
		t.Parallel()

		sm := newServiceMap[string, *dummyService](events.NoopLogger)
		sup.Add(sm)

		// Add four services.

		d1 := newDummyService()
		d2 := newDummyService()
		d3 := newDummyService()
		d4 := newDummyService()

		sm.Add("keep1", d1)
		sm.Add("remove2", d2)
		sm.Add("keep3", d3)
		sm.Add("remove4", d4)

		<-d1.started
		<-d2.started
		<-d3.started
		<-d4.started

		// Remove two of them from within the iterator.

		sm.Each(func(k string, v *dummyService) error {
			if strings.HasPrefix(k, "remove") {
				sm.RemoveAndWait(k, 0)
			}
			return nil
		})

		// They should have stopped.

		<-d2.stopped
		<-d4.stopped

		// They should not be in the map anymore.

		if _, ok := sm.Get("remove2"); ok {
			t.Errorf("Service still in map")
		}
		if _, ok := sm.Get("remove4"); ok {
			t.Errorf("Service still in map")
		}

		// The other two should still be running.

		if _, ok := sm.Get("keep1"); !ok {
			t.Errorf("Service not in map")
		}
		if _, ok := sm.Get("keep3"); !ok {
			t.Errorf("Service not in map")
		}
	})
}

type dummyService struct {
	started chan struct{}
	stopped chan struct{}
}

func newDummyService() *dummyService {
	return &dummyService{
		started: make(chan struct{}),
		stopped: make(chan struct{}),
	}
}

func (d *dummyService) Serve(ctx context.Context) error {
	close(d.started)
	defer close(d.stopped)
	<-ctx.Done()
	return nil
}
