// Copyright 2018 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 cmd

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

	"github.com/open-policy-agent/opa/internal/file/archive"
	"github.com/open-policy-agent/opa/v1/bundle"
	"github.com/open-policy-agent/opa/v1/keys"
	"github.com/open-policy-agent/opa/v1/util/test"
)

func TestWriteTokenToFile(t *testing.T) {

	token := `eyJhbGciOiJSUzI1NiJ9.eyJmaWxlcyI6W3sibmFtZSI6ImJ1bmRsZS8ubWFuaWZlc3QiLCJoYXNoIjoiZWUwZWRiZGZkMjgzNTBjNDk2ZjA4ODI3Y2E1Y2VhYjgwMzA2NzI0YjYyZGY1ZjY0MDRlNzBjYjc2NjYxNWQ5ZCIsImFsZ29yaXRobSI6IlNIQTI1NiJ9LHsibmFtZSI6ImJ1bmRsZS9odHRwL2V4YW1wbGUvYXV0aHovYXV0aHoucmVnbyIsImhhc2giOiI2MDJiZTcwMWIyYmE4ZTc3YTljNTNmOWIzM2QwZTkwM2MzNGMwMGMzMDkzM2Y2NDZiYmU3NGI3YzE2NGY2OGM2IiwiYWxnb3JpdGhtIjoiU0hBMjU2In0seyJuYW1lIjoiYnVuZGxlL3JvbGVzL2JpbmRpbmcvZGF0YS5qc29uIiwiaGFzaCI6ImIxODg1NTViZjczMGVlNDdkZjBiY2Y4MzVlYTNmNTQ1MjlmMzc4N2Y0ODQxZjFhZGE2MDM5M2RhYWZhZmJkYzciLCJhbGdvcml0aG0iOiJTSEEyNTYifV0sImtleWlkIjoiZm9vIiwic2NvcGUiOiJyZWFkIn0.YojuPnGWutdlDL7lwFGBXqPfDtxOG2BuZmShN5zm-G9zfMprI1AMqKDoPoNv4tuCGIBNXwoNsYHYiK538CHfJEfY1v4iDX3JFEWQlwx_CfJWDonwqT9SY9tHUW7PUUrI_WgJXZ5zei8RAMYMymKSb9hpSAtfGg_PU0kZr52WzjbPUj4SRiB19Swi61r0CFXYjbfx3GDJdjrGTNBSWrUCMrdhHYLEWqJPfSQ-fYfRrgQVhq3BJLwJJe66dgBEGnHEgA7XMuxkNIOv7mj3Y_EChbv2tjrD9NJPekDcYH1zCEc4BycHjNCcsGiQXDE6sFtoNZiCXLB2D0sLqUnBx4TCw27wTPfcOuL2KauLPahZitnH5mYvQD8NI76Pm4NSyJfevwdWjSsrT7vf0DCLS-dU6r9dJ79xM_hJU7136CT8ARcmSrk-EvCqfkrH2c4WwZyAzdyyyFumMZh4CYc2vcC7ap0NANHJT193fTud1i23mx1PBslwXdsIqXvBGlTbR7nb9o661m-B_mxbHMkG4nIeoGpZoaBJw8RVaA6-4D55gtk8aaMyLJIlIIlV2_AKOLk3nPG3ACHiLSndasLDOIRIYkCluIEaM2FLEEPEtJfKNR6e1K-EK2TvNKMDAEUtJW71ggOuGQ3b5otYOoVVENJLwm-PsO7qb2Tq6PyAquI3ExU`
	expected := make(map[string]any)
	expected["signatures"] = []string{token}

	files := map[string]string{}

	test.WithTempFS(files, func(rootDir string) {
		err := writeTokenToFile(token, rootDir)
		if err != nil {
			t.Fatalf("Unexpected error %v", err)
		}

		bs, err := os.ReadFile(filepath.Join(rootDir, ".signatures.json"))
		if err != nil {
			t.Fatalf("Unexpected error %v", err)
		}

		expectedBytes, err := json.MarshalIndent(expected, "", " ")
		if err != nil {
			t.Fatal(err)
		}

		if !bytes.Equal(expectedBytes, bs) {
			t.Fatal("Unexpected content in \".signatures.json\" file")
		}
	})
}

func TestDoSign(t *testing.T) {
	files := map[string]string{
		"foo/bar/data.json":     `{"y": 2}`,
		"/example/example.rego": `package example`,
		"/.signatures.json":     `{"signatures": []}`,
	}
	test.WithTempFS(files, func(rootDir string) {
		params := signCmdParams{
			algorithm:      "HS256",
			key:            "mysecret",
			outputFilePath: rootDir,
			bundleMode:     true,
		}

		err := doSign([]string{rootDir}, params)
		if err != nil {
			t.Fatalf("Unexpected error %v", err)
		}
	})
}

func TestBundleSignVerification(t *testing.T) {

	// files to be included in the bundle
	files := map[string]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}}}`,
	}

	test.WithTempFS(files, func(rootDir string) {
		params := signCmdParams{
			algorithm:      "HS256",
			key:            "mysecret",
			outputFilePath: rootDir,
			bundleMode:     true,
		}

		err := doSign([]string{rootDir}, params)
		if err != nil {
			t.Fatalf("Unexpected error %v", err)
		}

		// create gzipped tarball
		var filesInBundle [][2]string
		err = filepath.Walk(rootDir, func(path string, info os.FileInfo, _ error) error {
			if !info.IsDir() {
				bs, err := os.ReadFile(path)
				if err != nil {
					return err
				}
				filesInBundle = append(filesInBundle, [2]string{path, string(bs)})
			}
			return nil
		})
		if err != nil {
			t.Fatal(err)
		}

		buf := archive.MustWriteTarGz(filesInBundle)

		// bundle verification config
		kc := keys.Config{
			Key:       "mysecret",
			Algorithm: "HS256",
		}

		bvc := bundle.NewVerificationConfig(map[string]*keys.Config{"foo": &kc}, "foo", "", nil)
		reader := bundle.NewReader(buf).WithBundleVerificationConfig(bvc).WithBaseDir(rootDir)

		_, err = reader.Read()
		if err != nil {
			t.Fatalf("Unexpected error %v", err)
		}
	})
}

func TestValidateSignParams(t *testing.T) {

	tests := map[string]struct {
		args    []string
		params  signCmdParams
		wantErr bool
		err     error
	}{
		"no_args": {
			[]string{},
			newSignCmdParams(),
			true, errors.New("specify atleast one path containing policy and/or data files"),
		},
		"no_signing_key": {
			[]string{"foo"},
			newSignCmdParams(),
			true, errors.New("specify the secret (HMAC) or path of the PEM file containing the private key (RSA and ECDSA)"),
		},
		"empty_signing_key": {
			[]string{"foo"},
			signCmdParams{key: "", bundleMode: true},
			true, errors.New("specify the secret (HMAC) or path of the PEM file containing the private key (RSA and ECDSA)"),
		},
		"non_bundle_mode": {
			[]string{"foo"},
			signCmdParams{key: "foo"},
			true, errors.New("enable bundle mode (ie. --bundle) to sign bundle files or directories"),
		},
		"no_error": {
			[]string{"foo"},
			signCmdParams{key: "foo", bundleMode: true},
			false, nil,
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {

			err := validateSignParams(tc.args, tc.params)

			if tc.wantErr {
				if err == nil {
					t.Fatal("Expected error but got nil")
				}

				if tc.err != nil && tc.err.Error() != err.Error() {
					t.Fatalf("Expected error message %v but got %v", tc.err.Error(), err.Error())
				}
			} else if err != nil {
				t.Fatalf("Unexpected error %v", err)
			}
		})
	}
}
