package main

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	merkledag "github.com/ipfs/boxo/ipld/merkledag"
	ipld "github.com/ipfs/go-ipld-format"
	config "github.com/ipfs/kubo/config"
	"github.com/libp2p/go-libp2p/core/host"
	peer "github.com/libp2p/go-libp2p/core/peer"
)

type testPinMFSContext struct {
	ctx context.Context
	cfg *config.Config
	err error
}

func (x *testPinMFSContext) Context() context.Context {
	return x.ctx
}

func (x *testPinMFSContext) GetConfig() (*config.Config, error) {
	return x.cfg, x.err
}

type testPinMFSNode struct {
	err error
}

func (x *testPinMFSNode) RootNode() (ipld.Node, error) {
	return merkledag.NewRawNode([]byte{0x01}), x.err
}

func (x *testPinMFSNode) Identity() peer.ID {
	return peer.ID("test_id")
}

func (x *testPinMFSNode) PeerHost() host.Host {
	return nil
}

var testConfigPollInterval = time.Second

func isErrorSimilar(e1, e2 error) bool {
	switch {
	case e1 == nil && e2 == nil:
		return true
	case e1 != nil && e2 == nil:
		return false
	case e1 == nil && e2 != nil:
		return false
	default:
		return strings.Contains(e1.Error(), e2.Error()) || strings.Contains(e2.Error(), e1.Error())
	}
}

func TestPinMFSConfigError(t *testing.T) {
	ctx := &testPinMFSContext{
		ctx: context.Background(),
		cfg: nil,
		err: fmt.Errorf("couldn't read config"),
	}
	node := &testPinMFSNode{}
	errCh := make(chan error)
	go pinMFSOnChange(testConfigPollInterval, ctx, node, errCh)
	if !isErrorSimilar(<-errCh, ctx.err) {
		t.Errorf("error did not propagate")
	}
	if !isErrorSimilar(<-errCh, ctx.err) {
		t.Errorf("error did not propagate")
	}
}

func TestPinMFSRootNodeError(t *testing.T) {
	ctx := &testPinMFSContext{
		ctx: context.Background(),
		cfg: &config.Config{
			Pinning: config.Pinning{},
		},
		err: nil,
	}
	node := &testPinMFSNode{
		err: fmt.Errorf("cannot create root node"),
	}
	errCh := make(chan error)
	go pinMFSOnChange(testConfigPollInterval, ctx, node, errCh)
	if !isErrorSimilar(<-errCh, node.err) {
		t.Errorf("error did not propagate")
	}
	if !isErrorSimilar(<-errCh, node.err) {
		t.Errorf("error did not propagate")
	}
}

func TestPinMFSService(t *testing.T) {
	cfgInvalidInterval := &config.Config{
		Pinning: config.Pinning{
			RemoteServices: map[string]config.RemotePinningService{
				"disabled": {
					Policies: config.RemotePinningServicePolicies{
						MFS: config.RemotePinningServiceMFSPolicy{
							Enable: false,
						},
					},
				},
				"invalid_interval": {
					Policies: config.RemotePinningServicePolicies{
						MFS: config.RemotePinningServiceMFSPolicy{
							Enable:        true,
							RepinInterval: "INVALID_INTERVAL",
						},
					},
				},
			},
		},
	}
	cfgValidUnnamed := &config.Config{
		Pinning: config.Pinning{
			RemoteServices: map[string]config.RemotePinningService{
				"valid_unnamed": {
					Policies: config.RemotePinningServicePolicies{
						MFS: config.RemotePinningServiceMFSPolicy{
							Enable:        true,
							PinName:       "",
							RepinInterval: "2s",
						},
					},
				},
			},
		},
	}
	cfgValidNamed := &config.Config{
		Pinning: config.Pinning{
			RemoteServices: map[string]config.RemotePinningService{
				"valid_named": {
					Policies: config.RemotePinningServicePolicies{
						MFS: config.RemotePinningServiceMFSPolicy{
							Enable:        true,
							PinName:       "pin_name",
							RepinInterval: "2s",
						},
					},
				},
			},
		},
	}
	testPinMFSServiceWithError(t, cfgInvalidInterval, "remote pinning service \"invalid_interval\" has invalid MFS.RepinInterval")
	testPinMFSServiceWithError(t, cfgValidUnnamed, "error while listing remote pins: empty response from remote pinning service")
	testPinMFSServiceWithError(t, cfgValidNamed, "error while listing remote pins: empty response from remote pinning service")
}

func testPinMFSServiceWithError(t *testing.T, cfg *config.Config, expectedErrorPrefix string) {
	goctx, cancel := context.WithCancel(context.Background())
	ctx := &testPinMFSContext{
		ctx: goctx,
		cfg: cfg,
		err: nil,
	}
	node := &testPinMFSNode{
		err: nil,
	}
	errCh := make(chan error)
	go pinMFSOnChange(testConfigPollInterval, ctx, node, errCh)
	defer cancel()
	// first pass through the pinning loop
	err := <-errCh
	if !strings.Contains((err).Error(), expectedErrorPrefix) {
		t.Errorf("expecting error containing %q", expectedErrorPrefix)
	}
	// second pass through the pinning loop
	if !strings.Contains((err).Error(), expectedErrorPrefix) {
		t.Errorf("expecting error containing %q", expectedErrorPrefix)
	}
}
