package graph_path_finder

import (
	"errors"
	"slices"
	"testing"

	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/discovery"
	"github.com/tech/sendico/pkg/merrors"
)

func TestFind_ValidatesInput(t *testing.T) {
	finder := New()

	_, err := finder.Find(FindInput{
		DestinationRail: discovery.RailCardPayout,
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for missing source rail, got %v", err)
	}

	_, err = finder.Find(FindInput{
		SourceRail: discovery.RailCrypto,
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for missing destination rail, got %v", err)
	}
}

func TestFind_SourceEqualsDestination(t *testing.T) {
	finder := New()

	path, err := finder.Find(FindInput{
		SourceRail:      discovery.RailCrypto,
		DestinationRail: discovery.RailCrypto,
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if path == nil {
		t.Fatalf("expected path")
	}
	if got, want := railsToStrings(path.Rails), []string{"CRYPTO"}; !equalStrings(got, want) {
		t.Fatalf("unexpected rails: got=%v want=%v", got, want)
	}
	if len(path.Edges) != 0 {
		t.Fatalf("expected no edges for same source and destination")
	}
}

func TestFind_FindsIndirectPath(t *testing.T) {
	finder := New()

	path, err := finder.Find(FindInput{
		SourceRail:      discovery.RailCrypto,
		DestinationRail: discovery.RailCardPayout,
		Edges: []Edge{
			{FromRail: discovery.RailCrypto, ToRail: discovery.RailLedger},
			{FromRail: discovery.RailLedger, ToRail: discovery.RailCardPayout},
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	assertPathRails(t, path, []string{"CRYPTO", "LEDGER", "CARD"})
	if got, want := len(path.Edges), 2; got != want {
		t.Fatalf("unexpected edge count: got=%d want=%d", got, want)
	}
}

func TestFind_PrefersShortestPath(t *testing.T) {
	finder := New()

	path, err := finder.Find(FindInput{
		SourceRail:      discovery.RailCrypto,
		DestinationRail: discovery.RailCardPayout,
		Edges: []Edge{
			{FromRail: discovery.RailCrypto, ToRail: discovery.RailCardPayout},
			{FromRail: discovery.RailCrypto, ToRail: discovery.RailLedger},
			{FromRail: discovery.RailLedger, ToRail: discovery.RailCardPayout},
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	assertPathRails(t, path, []string{"CRYPTO", "CARD"})
}

func TestFind_HandlesCycles(t *testing.T) {
	finder := New()

	path, err := finder.Find(FindInput{
		SourceRail:      discovery.RailCrypto,
		DestinationRail: discovery.RailCardPayout,
		Edges: []Edge{
			{FromRail: discovery.RailCrypto, ToRail: discovery.RailLedger},
			{FromRail: discovery.RailLedger, ToRail: discovery.RailCrypto},
			{FromRail: discovery.RailLedger, ToRail: discovery.RailCardPayout},
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	assertPathRails(t, path, []string{"CRYPTO", "LEDGER", "CARD"})
}

func TestFind_ReturnsErrorWhenPathUnavailable(t *testing.T) {
	finder := New()

	_, err := finder.Find(FindInput{
		SourceRail:      discovery.RailCrypto,
		DestinationRail: discovery.RailCardPayout,
		Edges: []Edge{
			{FromRail: discovery.RailCrypto, ToRail: discovery.RailLedger},
		},
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for unavailable path, got %v", err)
	}
}

func assertPathRails(t *testing.T, path *Path, want []string) {
	t.Helper()
	if path == nil {
		t.Fatalf("expected non-nil path")
	}
	got := railsToStrings(path.Rails)
	if !equalStrings(got, want) {
		t.Fatalf("unexpected rails: got=%v want=%v", got, want)
	}
}

func railsToStrings(rails []model.Rail) []string {
	result := make([]string, 0, len(rails))
	for _, rail := range rails {
		result = append(result, string(rail))
	}
	return result
}

func equalStrings(got, want []string) bool {
	return slices.Equal(got, want)
}
