// Copyright 2019 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 mock defines a fake storage implementation for use in testing.
package mock

import (
	"context"
	"fmt"
	"testing"

	"github.com/open-policy-agent/opa/v1/storage"
	"github.com/open-policy-agent/opa/v1/storage/inmem"
)

// Transaction is a mock storage.Transaction implementation for use in testing.
// It uses an internal storage.Transaction pointer with some added functionality.
type Transaction struct {
	txn       storage.Transaction
	Committed int
	Aborted   int
}

// ID returns the underlying transaction ID
func (t *Transaction) ID() uint64 {
	return t.txn.ID()
}

// Validate returns an error if the transaction is in an invalid state
func (t *Transaction) Validate() error {
	if t.Committed > 1 {
		return fmt.Errorf("transaction %d has too many commits (%d)", t.ID(), t.Committed)
	}
	if t.Aborted > 1 {
		return fmt.Errorf("transaction %d has too many aborts (%d)", t.ID(), t.Committed)
	}
	return nil
}

func (t *Transaction) safeToUse() bool {
	return t.Committed == 0 && t.Aborted == 0
}

// Store is a mock storage.Store implementation for use in testing.
type Store struct {
	inmem        storage.Store
	storeOpts    []inmem.Opt
	baseData     map[string]any
	Transactions []*Transaction
	Reads        []*ReadCall
	Writes       []*WriteCall
}

// ReadCall captures the parameters for a Read call
type ReadCall struct {
	Transaction *Transaction
	Path        storage.Path
	Error       error
	Safe        bool
}

// WriteCall captures the parameters for a write call
type WriteCall struct {
	Transaction *Transaction
	Op          storage.PatchOp
	Path        storage.Path
	Error       error
	Safe        bool
}

// New creates a new mock Store
func New(opt ...inmem.Opt) *Store {
	s := &Store{
		storeOpts: opt,
	}
	s.Reset()
	return s
}

// NewWithData creates a store with some initial data
func NewWithData(data map[string]any, opt ...inmem.Opt) *Store {
	s := &Store{
		baseData:  data,
		storeOpts: opt,
	}
	s.Reset()
	return s
}

// Reset the store
func (s *Store) Reset() {
	s.Transactions = []*Transaction{}
	s.Reads = []*ReadCall{}
	s.Writes = []*WriteCall{}
	if s.baseData != nil {
		s.inmem = inmem.NewFromObjectWithOpts(s.baseData, s.storeOpts...)
	} else {
		s.inmem = inmem.NewWithOpts(s.storeOpts...)
	}
}

// GetTransaction will a transaction with a specific ID
// that was associated with this Store.
func (s *Store) GetTransaction(id uint64) *Transaction {
	for _, txn := range s.Transactions {
		if txn.ID() == id {
			return txn
		}
	}
	return nil
}

// Errors returns a list of errors for each invalid state found.
// If any Transactions are invalid or reads/writes were
// unsafe an error will be returned for each problem.
func (s *Store) Errors() []error {
	var errs []error
	for _, txn := range s.Transactions {
		err := txn.Validate()
		if err != nil {
			errs = append(errs, err)
		}
	}

	for _, read := range s.Reads {
		if !read.Safe {
			errs = append(errs, fmt.Errorf("unsafe Read call %+v", *read))
		}
	}

	for _, write := range s.Writes {
		if !write.Safe {
			errs = append(errs, fmt.Errorf("unsafe Write call %+v", *write))
		}
	}

	return errs
}

// AssertValid will raise an error with the provided testing.T if
// there are any errors on the store.
func (s *Store) AssertValid(t *testing.T) {
	t.Helper()
	for _, err := range s.Errors() {
		t.Errorf("Error detected on store: %s", err)
	}
}

// storage.Store interface implementation

// Register just shims the call to the underlying inmem store
func (s *Store) Register(ctx context.Context, txn storage.Transaction, config storage.TriggerConfig) (storage.TriggerHandle, error) {
	return s.inmem.Register(ctx, getRealTxn(txn), config)
}

// ListPolicies just shims the call to the underlying inmem store
func (s *Store) ListPolicies(ctx context.Context, txn storage.Transaction) ([]string, error) {
	return s.inmem.ListPolicies(ctx, getRealTxn(txn))
}

// GetPolicy just shims the call to the underlying inmem store
func (s *Store) GetPolicy(ctx context.Context, txn storage.Transaction, name string) ([]byte, error) {
	return s.inmem.GetPolicy(ctx, getRealTxn(txn), name)
}

// UpsertPolicy just shims the call to the underlying inmem store
func (s *Store) UpsertPolicy(ctx context.Context, txn storage.Transaction, name string, policy []byte) error {
	return s.inmem.UpsertPolicy(ctx, getRealTxn(txn), name, policy)
}

// DeletePolicy just shims the call to the underlying inmem store
func (s *Store) DeletePolicy(ctx context.Context, txn storage.Transaction, name string) error {
	return s.inmem.DeletePolicy(ctx, getRealTxn(txn), name)
}

// NewTransaction will create a new transaction on the underlying inmem store
// but wraps it with a mock Transaction. These are then tracked on the store.
func (s *Store) NewTransaction(ctx context.Context, params ...storage.TransactionParams) (storage.Transaction, error) {
	realTxn, err := s.inmem.NewTransaction(ctx, params...)
	if err != nil {
		return nil, err
	}
	txn := &Transaction{
		txn:       realTxn,
		Committed: 0,
		Aborted:   0,
	}
	s.Transactions = append(s.Transactions, txn)
	return txn, nil
}

// Read will make a read from the underlying inmem store and
// add a new entry to the mock store Reads list. If there
// is an error are the read is unsafe it will be noted in
// the ReadCall.
func (s *Store) Read(ctx context.Context, txn storage.Transaction, path storage.Path) (any, error) {
	mockTxn := txn.(*Transaction)

	data, err := s.inmem.Read(ctx, mockTxn.txn, path)

	s.Reads = append(s.Reads, &ReadCall{
		Transaction: mockTxn,
		Path:        path,
		Error:       err,
		Safe:        mockTxn.safeToUse(),
	})

	return data, err
}

// Write will make a read from the underlying inmem store and
// add a new entry to the mock store Writes list. If there
// is an error are the write is unsafe it will be noted in
// the WriteCall.
func (s *Store) Write(ctx context.Context, txn storage.Transaction, op storage.PatchOp, path storage.Path, value any) error {
	mockTxn := txn.(*Transaction)

	err := s.inmem.Write(ctx, mockTxn.txn, op, path, value)

	s.Writes = append(s.Writes, &WriteCall{
		Transaction: mockTxn,
		Op:          op,
		Path:        path,
		Error:       err,
		Safe:        mockTxn.safeToUse(),
	})

	return nil
}

// Commit will commit the underlying transaction while
// also updating the mock Transaction
func (s *Store) Commit(ctx context.Context, txn storage.Transaction) error {
	mockTxn := txn.(*Transaction)

	err := s.inmem.Commit(ctx, mockTxn.txn)
	if err != nil {
		return err
	}

	mockTxn.Committed++
	return nil
}

// Abort will abort the underlying transaction while
// also updating the mock Transaction
func (s *Store) Abort(ctx context.Context, txn storage.Transaction) {
	mockTxn := txn.(*Transaction)
	s.inmem.Abort(ctx, mockTxn.txn)
	mockTxn.Aborted++
}

func (s *Store) Truncate(ctx context.Context, txn storage.Transaction, params storage.TransactionParams, it storage.Iterator) error {
	return s.inmem.Truncate(ctx, getRealTxn(txn), params, it)
}

func getRealTxn(txn storage.Transaction) storage.Transaction {
	return txn.(*Transaction).txn
}
