// Copyright 2020 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package bundle

import (
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

	"github.com/open-policy-agent/opa/v1/util/test"

	"github.com/lestrrat-go/jwx/v3/jwa"
	"github.com/lestrrat-go/jwx/v3/jws"
)

func TestGenerateSignedToken(t *testing.T) {

	files := [][2]string{
		{"/.manifest", `{"revision": "quickbrownfaux"}`},
		{"/a/b/c/data.json", "[1,2,3]"},
		{"/a/b/d/data.json", "true"},
		{"/a/b/y/data.yaml", `foo: 1`},
		{"/example/example.rego", `package example`},
		{"/policy.wasm", `modules-compiled-as-wasm-binary`},
		{"/data.json", `{"x": {"y": true}, "a": {"b": {"z": true}}}`},
	}

	input := []FileInfo{}

	expDigests := make([]string, len(files))
	expDigests[0] = "a005c38a509dc2d5a7407b9494efb2ad"
	expDigests[1] = "60f7b5dc86ded48785436192a08dbfd04894d7f1b417c4f8d3714679a7f78cb3c833f16a8559a1cf1f32968747dc1d95ef34826263dacf125ded8f5c374be4c0"
	expDigests[2] = "b326b5062b2f0e69046810717534cb09"
	expDigests[3] = "20f27a640a233e6524fe7d138898583cd43475724806feb26be7f214e1d10b29edf6a0d3cb08f82107a45686b61b8fdabab6406cf4e70efe134f42238dbd70ab"
	expDigests[4] = "655578028abb7b9006e93aff9dda8620"
	expDigests[5] = "6347e9be8e3051dc054fbbd3db72fb3f7ae7051c4ef6353e29895aa495452179e10e434fb4a60316e06916464bcc5d4ecabbb2797e04c0213943cf8e69f4c0ae"
	expDigests[6] = "36669864a622563256817033b1fc53db"

	for i, f := range files {
		file := FileInfo{
			Name: f[0],
			Hash: expDigests[i],
		}

		if i%2 == 0 {
			file.Algorithm = MD5.String()
		} else {
			file.Algorithm = SHA512.String()
		}

		input = append(input, file)
	}

	sc := NewSigningConfig("secret", "HS256", "")
	token, err := GenerateSignedToken(input, sc, "")
	if err != nil {
		t.Fatalf("Unexpected error %v", err)
	}

	// verify the signed token
	alg := jwa.HS256()
	_, err = jws.Verify([]byte(token), jws.WithKey(alg, []byte("secret")))
	if err != nil {
		t.Fatalf("Unexpected error %v", err)
	}
}

func TestGenerateSignedTokenWithClaims(t *testing.T) {

	files := [][2]string{
		{"/.manifest", `{"revision": "quickbrownfaux"}`},
		{"/a/b/d/data.json", "true"},
		{"/example/example.rego", `package example`},
		{"/data.json", `{"x": {"y": true}, "a": {"b": {"z": true}}}}`},
	}

	input := []FileInfo{}

	expDigests := make([]string, len(files))
	expDigests[0] = "a005c38a509dc2d5a7407b9494efb2ad"
	expDigests[1] = "b326b5062b2f0e69046810717534cb09"
	expDigests[2] = "655578028abb7b9006e93aff9dda8620"
	expDigests[3] = "36669864a622563256817033b1fc53db"

	for i, f := range files {
		file := FileInfo{
			Name:      f[0],
			Hash:      expDigests[i],
			Algorithm: MD5.String(),
		}
		input = append(input, file)
	}

	test.WithTempFS(map[string]string{}, func(rootDir string) {
		claims := make(map[string]any)
		claims["scope"] = "read"

		claimBytes, err := json.Marshal(claims)
		if err != nil {
			t.Fatalf("Unexpected error %v", err)
		}

		// create claims file
		claimsFile := filepath.Join(rootDir, "claims.json")
		if err := os.WriteFile(claimsFile, claimBytes, 0644); err != nil {
			t.Fatalf("Unexpected error: %s", err)
		}

		keyid := "foo"

		sc := NewSigningConfig("secret", "HS256", filepath.Join(rootDir, "claims.json"))

		token, err := GenerateSignedToken(input, sc, keyid)
		if err != nil {
			t.Fatalf("Unexpected error %v", err)
		}

		// verify the signed token
		alg := jwa.HS256()
		_, err = jws.Verify([]byte(token), jws.WithKey(alg, []byte("secret")))
		if err != nil {
			t.Fatalf("Unexpected error %v", err)
		}

		// check the kid is in the header
		m, err := jws.ParseString(token)
		if err != nil {
			t.Fatalf("Unexpected error %v", err)
		}

		signatures := m.Signatures()
		if len(signatures) != 1 {
			t.Fatalf("There should be exactly one signature, got %d", len(signatures))
		}

		if v, ok := signatures[0].ProtectedHeaders().KeyID(); !ok || v != keyid {
			t.Errorf("key id not set")
		}
	})
}

func TestGeneratePayload(t *testing.T) {

	files := [][2]string{
		{"/.manifest", `{"revision": "quickbrownfaux"}`},
	}

	input := []FileInfo{}

	file := FileInfo{
		Name:      files[0][0],
		Hash:      "a005c38a509dc2d5a7407b9494efb2ad",
		Algorithm: MD5.String(),
	}
	input = append(input, file)

	sc := NewSigningConfig("secret", "HS256", "")
	keyID := "default"

	// non-empty key id
	token, err := generateToken(input, sc, keyID)
	if err != nil {
		t.Fatalf("Unexpected error %v", err)
	}

	var gotKid string
	if err := token.Get("keyid", &gotKid); err != nil {
		t.Fatalf("Expected claim \"keyid\" in token: %s", err)
	}

	if gotKid != keyID {
		t.Fatalf("Expected key id %v but got %v", keyID, gotKid)
	}

	// empty key id
	token, err = generateToken(input, sc, "")
	if err != nil {
		t.Fatalf("Unexpected error %v", err)
	}

	if err := token.Get("keyid", &gotKid); err == nil {
		t.Fatal("Unexpected claim \"keyid\" in token")
	}
}

type CustomSigner struct{}

func (*CustomSigner) GenerateSignedToken(_ []FileInfo, _ *SigningConfig, _ string) (string, error) {
	return "", nil
}

func TestCustomSigner(t *testing.T) {
	custom := &CustomSigner{}
	err := RegisterSigner(defaultSignerID, custom)
	if err == nil {
		t.Fatalf("Expected error when registering with default ID")
	}
	if err := RegisterSigner("_test", custom); err != nil {
		t.Fatal(err)
	}
	defaultSigner, err := GetSigner(defaultSignerID)
	if err != nil {
		t.Fatalf("Unexpected error %v", err)
	}
	if _, isDefault := defaultSigner.(*DefaultSigner); !isDefault {
		t.Fatalf("Expected DefaultSigner to be registered at key %s", defaultSignerID)
	}
	customSigner, err := GetSigner("_test")
	if err != nil {
		t.Fatalf("Unexpected error %v", err)
	}
	if _, isCustom := customSigner.(*CustomSigner); !isCustom {
		t.Fatalf("Expected CustomSigner to be registered at key _test")
	}
	if _, err = GetSigner("_unregistered"); err == nil {
		t.Fatalf("Expected error when no Signer exists at provided key")
	}
}
