package sourcepolicy

import (
	"testing"

	"github.com/moby/buildkit/solver/pb"
	spb "github.com/moby/buildkit/sourcepolicy/pb"
	"github.com/moby/buildkit/util/bklog"
	"github.com/sirupsen/logrus"
	"github.com/stretchr/testify/require"
)

func TestEngineEvaluate(t *testing.T) {
	t.Run("Deny All", testDenyAll)
	t.Run("Allow Deny", testAllowDeny)
	t.Run("Convert", testConvert)
	t.Run("Convert exact", testConvertExact)
	t.Run("Convert Deny", testConvertDeny)
	t.Run("Allow Convert Deny", testAllowConvertDeny)
	t.Run("Test convert loop", testConvertLoop)
	t.Run("Test convert http", testConvertHTTP)
	t.Run("Test convert regex", testConvertRegex)
	t.Run("Test convert wildcard", testConvertWildcard)
	t.Run("Test convert multiple", testConvertMultiple)
	t.Run("test multiple policies", testMultiplePolicies)
	t.Run("Last rule wins", testLastRuleWins)
}

func testLastRuleWins(t *testing.T) {
	pol := []*spb.Policy{
		{
			Rules: []*spb.Rule{
				{
					Action: spb.PolicyAction_ALLOW,
					Selector: &spb.Selector{
						Identifier: "docker-image://docker.io/library/busybox:latest",
					},
				},
				{
					Action: spb.PolicyAction_DENY,
					Selector: &spb.Selector{
						Identifier: "docker-image://docker.io/library/busybox:latest",
					},
				},
				{
					Action: spb.PolicyAction_ALLOW,
					Selector: &spb.Selector{
						Identifier: "docker-image://docker.io/library/busybox:latest",
					},
				},
			},
		},
	}

	e := NewEngine(pol)
	mut, err := e.Evaluate(t.Context(), &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/busybox:latest",
	})
	require.NoError(t, err)
	require.False(t, mut)
}

func testMultiplePolicies(t *testing.T) {
	pol := []*spb.Policy{
		{
			Rules: []*spb.Rule{
				{
					Action: spb.PolicyAction_ALLOW,
					Selector: &spb.Selector{
						Identifier: "docker-image://docker.io/library/busybox:latest",
					},
				},
			},
		},
		{
			Rules: []*spb.Rule{
				{
					Action: spb.PolicyAction_DENY,
					Selector: &spb.Selector{
						Identifier: "docker-image://docker.io/library/busybox:latest",
					},
				},
			},
		},
	}

	e := NewEngine(pol)
	mut, err := e.Evaluate(t.Context(), &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/busybox:latest",
	})
	require.ErrorIs(t, err, ErrSourceDenied)
	require.False(t, mut)
}

func testConvertMultiple(t *testing.T) {
	pol := []*spb.Policy{
		{
			Rules: []*spb.Rule{
				{
					Action: spb.PolicyAction_CONVERT,
					Selector: &spb.Selector{
						Identifier: "docker-image://docker.io/library/busybox:latest",
					},
					Updates: &spb.Update{
						Identifier: "docker-image://docker.io/library/alpine:latest",
					},
				},
				{
					Action: spb.PolicyAction_CONVERT,
					Selector: &spb.Selector{
						Identifier: "docker-image://docker.io/library/alpine:latest",
					},
					Updates: &spb.Update{
						Identifier: "docker-image://docker.io/library/debian:buster",
					},
				},
				{
					Action: spb.PolicyAction_CONVERT,
					Selector: &spb.Selector{
						Identifier: "docker-image://docker.io/library/debian:buster",
					},
					Updates: &spb.Update{
						Identifier: "docker-image://docker.io/library/debian:bullseye",
					},
				},
			},
		},
	}

	op := &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/busybox:latest",
	}

	ctx := t.Context()
	e := NewEngine(pol)

	mutated, err := e.Evaluate(ctx, op)
	require.True(t, mutated)
	require.NoError(t, err)
}

func testConvertWildcard(t *testing.T) {
	pol := []*spb.Policy{
		{
			Rules: []*spb.Rule{
				{
					Action: spb.PolicyAction_CONVERT,
					Selector: &spb.Selector{
						Identifier: "docker-image://docker.io/library/golang:*",
						MatchType:  spb.MatchType_WILDCARD,
					},
					Updates: &spb.Update{
						Identifier: "docker-image://fakereg.io/library/golang:${1}",
					},
				},
			},
		},
	}

	op := &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/golang:1.19",
	}

	ctx := t.Context()
	e := NewEngine(pol)

	mutated, err := e.Evaluate(ctx, op)
	require.True(t, mutated)
	require.NoError(t, err)
	require.Equal(t, "docker-image://fakereg.io/library/golang:1.19", op.Identifier)
}

func testConvertRegex(t *testing.T) {
	pol := &spb.Policy{
		Rules: []*spb.Rule{
			{
				Action: spb.PolicyAction_CONVERT,
				Selector: &spb.Selector{
					Identifier: `docker\-image://docker\.io/library/golang:(.*)`,
					MatchType:  spb.MatchType_REGEX,
				},
				Updates: &spb.Update{
					Identifier: "docker-image://fakereg.io/library/golang:${1}",
				},
			},
		},
	}

	op := &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/golang:1.19",
	}

	ctx := t.Context()
	e := NewEngine([]*spb.Policy{pol})

	mutated, err := e.Evaluate(ctx, op)
	require.True(t, mutated)
	require.NoError(t, err)
	require.Equal(t, "docker-image://fakereg.io/library/golang:1.19", op.Identifier)
}

func testConvertHTTP(t *testing.T) {
	pol := &spb.Policy{
		Rules: []*spb.Rule{
			{
				Action: spb.PolicyAction_CONVERT,
				Selector: &spb.Selector{
					Identifier: "https://example.com/foo",
				},
				Updates: &spb.Update{
					Attrs: map[string]string{"http.checksum": "sha256:1234"},
				},
			},
		},
	}

	op := &pb.SourceOp{
		Identifier: "https://example.com/foo",
	}

	ctx := t.Context()
	e := NewEngine([]*spb.Policy{pol})

	mutated, err := e.Evaluate(ctx, op)
	require.True(t, mutated)
	require.NoError(t, err)
	require.Equal(t, "https://example.com/foo", op.Identifier)
}

func testConvertLoop(t *testing.T) {
	pol := &spb.Policy{
		Rules: []*spb.Rule{
			{
				Action: spb.PolicyAction_CONVERT,
				Selector: &spb.Selector{
					Identifier: "docker-image://docker.io/library/busybox:latest",
				},
				Updates: &spb.Update{
					Identifier: "docker-image://docker.io/library/alpine:latest",
				},
			},
			{
				Action: spb.PolicyAction_CONVERT,
				Selector: &spb.Selector{
					Identifier: "docker-image://docker.io/library/alpine:latest",
				},
				Updates: &spb.Update{
					Identifier: "docker-image://docker.io/library/busybox:latest",
				},
			},
		},
	}

	op := &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/busybox:latest",
	}

	ctx := t.Context()
	e := NewEngine([]*spb.Policy{pol})

	mutated, err := e.Evaluate(ctx, op)
	require.True(t, mutated)
	require.ErrorIs(t, err, ErrTooManyOps)
}

func testAllowConvertDeny(t *testing.T) {
	pol := &spb.Policy{
		Rules: []*spb.Rule{
			{
				Action: spb.PolicyAction_CONVERT,
				Selector: &spb.Selector{
					Identifier: "docker-image://docker.io/library/busybox:latest",
				},
				Updates: &spb.Update{
					Identifier: "docker-image://docker.io/library/alpine:latest",
				},
			},
			{
				Action: spb.PolicyAction_ALLOW,
				Selector: &spb.Selector{
					Identifier: "docker-image://docker.io/library/alpine:latest",
				},
			},
			{
				Action: spb.PolicyAction_DENY,
				Selector: &spb.Selector{
					Identifier: "docker-image://docker.io/library/alpine:*",
				},
			},
			{
				Action: spb.PolicyAction_DENY,
				Selector: &spb.Selector{
					Identifier: "docker-image://docker.io/library/busybox:latest",
				},
			},
		},
	}

	op := &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/busybox:latest",
	}

	ctx := t.Context()
	e := NewEngine([]*spb.Policy{pol})

	mutated, err := e.Evaluate(ctx, op)
	require.True(t, mutated)
	require.ErrorIs(t, err, ErrSourceDenied)
	require.Equal(t, "docker-image://docker.io/library/alpine:latest", op.Identifier)
}

func testConvertDeny(t *testing.T) {
	pol := &spb.Policy{
		Rules: []*spb.Rule{
			{
				Action: spb.PolicyAction_DENY,
				Selector: &spb.Selector{
					Identifier: "docker-image://docker.io/library/alpine:*",
				},
			},
			{
				Action: spb.PolicyAction_CONVERT,
				Selector: &spb.Selector{
					Identifier: "docker-image://docker.io/library/busybox:latest",
				},
				Updates: &spb.Update{
					Identifier: "docker-image://docker.io/library/alpine:latest",
				},
			},
		},
	}

	op := &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/busybox:latest",
	}

	ctx := t.Context()
	e := NewEngine([]*spb.Policy{pol})

	mutated, err := e.Evaluate(ctx, op)
	require.True(t, mutated)
	require.ErrorIs(t, err, ErrSourceDenied)
	require.Equal(t, "docker-image://docker.io/library/alpine:latest", op.Identifier)
}

func testConvert(t *testing.T) {
	cases := map[string]string{
		"docker-image://docker.io/library/busybox:latest": "docker-image://docker.io/library/alpine:latest",
		"docker-image://docker.io/library/alpine:latest":  "docker-image://docker.io/library/alpine:latest@sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
	}
	bklog.L.Logger.SetLevel(logrus.DebugLevel)

	for src, dst := range cases {
		t.Run(src+"=>"+dst, func(t *testing.T) {
			op := &pb.SourceOp{
				Identifier: src,
			}

			pol := &spb.Policy{
				Rules: []*spb.Rule{
					{
						Action: spb.PolicyAction_CONVERT,
						Selector: &spb.Selector{
							Identifier: src,
						},
						Updates: &spb.Update{
							Identifier: dst,
						},
					},
				},
			}

			ctx := t.Context()
			e := NewEngine([]*spb.Policy{pol})

			mutated, err := e.Evaluate(ctx, op)
			require.True(t, mutated)
			require.NoError(t, err)
			require.Equal(t, dst, op.Identifier)
		})
	}
}

func testConvertExact(t *testing.T) {
	src := "docker-image://docker.io/library/busybox:latest"
	dst := "docker-image://docker.io/library/busybox@sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c"
	op := &pb.SourceOp{
		Identifier: src,
	}

	pol := &spb.Policy{
		Rules: []*spb.Rule{
			{
				Action: spb.PolicyAction_CONVERT,
				Selector: &spb.Selector{
					Identifier: src,
					MatchType:  spb.MatchType_EXACT,
				},
				Updates: &spb.Update{
					Identifier: dst,
				},
			},
		},
	}

	mutated, err := NewEngine([]*spb.Policy{pol}).Evaluate(t.Context(), op)
	require.True(t, mutated)
	require.NoError(t, err)
	require.Equal(t, dst, op.Identifier)
}

func testAllowDeny(t *testing.T) {
	op := &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/alpine:latest",
	}
	pol := &spb.Policy{
		Rules: []*spb.Rule{
			{
				Action: spb.PolicyAction_ALLOW,
				Selector: &spb.Selector{
					Identifier: "docker-image://docker.io/library/alpine:latest",
				},
			},
			{
				Action: spb.PolicyAction_DENY,
				Selector: &spb.Selector{
					Identifier: "docker-image://*",
				},
			},
		},
	}

	ctx := t.Context()
	e := NewEngine([]*spb.Policy{pol})

	mutated, err := e.Evaluate(ctx, op)
	require.False(t, mutated)
	require.ErrorIs(t, err, ErrSourceDenied)

	op = &pb.SourceOp{
		Identifier: "docker-image://docker.io/library/busybox:latest",
	}

	mutated, err = e.Evaluate(ctx, op)
	require.False(t, mutated)
	require.ErrorIs(t, err, ErrSourceDenied)
}

func testDenyAll(t *testing.T) {
	cases := map[string]string{
		"docker-image": "docker-image://docker.io/library/alpine:latest",
		"https":        "https://github.com/moby/buildkit.git",
		"http":         "http://example.com",
	}

	for kind, ref := range cases {
		t.Run(ref, func(t *testing.T) {
			pol := &spb.Policy{
				Rules: []*spb.Rule{
					{
						Action: spb.PolicyAction_DENY,
						Selector: &spb.Selector{
							Identifier: kind + "://*",
						},
					},
				},
			}

			e := NewEngine([]*spb.Policy{pol})
			ctx := t.Context()

			op := &pb.SourceOp{
				Identifier: ref,
			}

			mutated, err := e.Evaluate(ctx, op)
			require.False(t, mutated)
			require.ErrorIs(t, err, ErrSourceDenied)
		})
	}
}
