// Copyright 2017 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package crossmodelrelations_test

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/juju/errors"
	"github.com/juju/names/v4"
	"github.com/juju/testing"
	"gopkg.in/macaroon-bakery.v2/bakery"
	"gopkg.in/macaroon-bakery.v2/bakery/checkers"
	"gopkg.in/macaroon.v2"

	"github.com/juju/juju/apiserver/authentication"
	commoncrossmodel "github.com/juju/juju/apiserver/common/crossmodel"
	"github.com/juju/juju/apiserver/common/firewall"
	"github.com/juju/juju/apiserver/facades/controller/crossmodelrelations"
	"github.com/juju/juju/charmstore"
	"github.com/juju/juju/core/crossmodel"
	corefirewall "github.com/juju/juju/core/network/firewall"
	"github.com/juju/juju/core/status"
	"github.com/juju/juju/core/watcher"
	"github.com/juju/juju/state"
	coretesting "github.com/juju/juju/testing"
)

type mockStatePool struct {
	st map[string]commoncrossmodel.Backend
}

func (st *mockStatePool) Get(modelUUID string) (commoncrossmodel.Backend, func(), error) {
	backend, ok := st.st[modelUUID]
	if !ok {
		return nil, nil, errors.NotFoundf("model for uuid %s", modelUUID)
	}
	return backend, func() {}, nil
}

type mockState struct {
	testing.Stub
	crossmodelrelations.CrossModelRelationsState
	relations             map[string]*mockRelation
	remoteApplications    map[string]*mockRemoteApplication
	applications          map[string]*mockApplication
	offers                map[string]*crossmodel.ApplicationOffer
	offerNames            map[string]string
	offerConnections      map[int]*mockOfferConnection
	offerConnectionsByKey map[string]*mockOfferConnection
	remoteEntities        map[names.Tag]string
	firewallRules         map[corefirewall.WellKnownServiceType]*state.FirewallRule
	ingressNetworks       map[string][]string
	migrationActive       bool
}

func newMockState() *mockState {
	return &mockState{
		relations:             make(map[string]*mockRelation),
		remoteApplications:    make(map[string]*mockRemoteApplication),
		applications:          make(map[string]*mockApplication),
		remoteEntities:        make(map[names.Tag]string),
		offers:                make(map[string]*crossmodel.ApplicationOffer),
		offerNames:            make(map[string]string),
		offerConnections:      make(map[int]*mockOfferConnection),
		offerConnectionsByKey: make(map[string]*mockOfferConnection),
		firewallRules:         make(map[corefirewall.WellKnownServiceType]*state.FirewallRule),
		ingressNetworks:       make(map[string][]string),
	}
}

func (st *mockState) ApplicationOfferForUUID(offerUUID string) (*crossmodel.ApplicationOffer, error) {
	offer, ok := st.offers[offerUUID]
	if !ok {
		return nil, errors.NotFoundf("offer %v", offerUUID)
	}
	return offer, nil
}

func (st *mockState) ApplicationOffer(offerName string) (*crossmodel.ApplicationOffer, error) {
	for _, offer := range st.offers {
		if offer.OfferName == offerName {
			return offer, nil
		}
	}
	return nil, errors.NotFoundf("offer %v", offerName)
}

func (st *mockState) ModelUUID() string {
	return coretesting.ModelTag.Id()
}

func (st *mockState) Model() (crossmodelrelations.Model, error) {
	return &mockModel{}, nil
}

func (st *mockState) AddRelation(eps ...state.Endpoint) (commoncrossmodel.Relation, error) {
	rel := &mockRelation{
		id:  len(st.relations),
		key: fmt.Sprintf("%v:%v %v:%v", eps[0].ApplicationName, eps[0].Name, eps[1].ApplicationName, eps[1].Name),
	}
	if _, ok := st.relations[rel.key]; ok {
		return nil, errors.AlreadyExistsf("relation %q", rel.key)
	}
	st.relations[rel.key] = rel
	return rel, nil
}

func (st *mockState) AddOfferConnection(arg state.AddOfferConnectionParams) (crossmodelrelations.OfferConnection, error) {
	if _, ok := st.offerConnections[arg.RelationId]; ok {
		return nil, errors.AlreadyExistsf("offer connection for relation %d", arg.RelationId)
	}
	oc := &mockOfferConnection{
		sourcemodelUUID: arg.SourceModelUUID,
		relationId:      arg.RelationId,
		relationKey:     arg.RelationKey,
		username:        arg.Username,
		offerUUID:       arg.OfferUUID,
	}
	st.offerConnections[arg.RelationId] = oc
	st.offerConnectionsByKey[arg.RelationKey] = oc
	return oc, nil
}

func (st *mockState) FirewallRule(service corefirewall.WellKnownServiceType) (*state.FirewallRule, error) {
	if r, ok := st.firewallRules[service]; ok {
		return r, nil
	}
	return nil, errors.NotFoundf("firewall rule for %v", service)
}

func (st *mockState) SaveIngressNetworks(relationKey string, cidrs []string) (state.RelationNetworks, error) {
	st.ingressNetworks[relationKey] = cidrs
	return nil, nil
}

func (st *mockState) OfferConnectionForRelation(relationKey string) (crossmodelrelations.OfferConnection, error) {
	oc, ok := st.offerConnectionsByKey[relationKey]
	if !ok {
		return nil, errors.NotFoundf("offer connection details for relation %v", relationKey)
	}
	return oc, nil
}

func (st *mockState) EndpointsRelation(eps ...state.Endpoint) (commoncrossmodel.Relation, error) {
	key := fmt.Sprintf("%v:%v %v:%v", eps[0].ApplicationName, eps[0].Name, eps[1].ApplicationName, eps[1].Name)
	if rel, ok := st.relations[key]; ok {
		return rel, nil
	}
	return nil, errors.NotFoundf("relation with key %q", key)
}

func (st *mockState) AddRemoteApplication(params state.AddRemoteApplicationParams) (commoncrossmodel.RemoteApplication, error) {
	app := &mockRemoteApplication{
		sourceModelUUID: params.SourceModel.Id(),
		consumerproxy:   params.IsConsumerProxy}
	st.remoteApplications[params.Name] = app
	return app, nil
}

func (st *mockState) OfferNameForRelation(key string) (string, error) {
	st.MethodCall(st, "OfferNameForRelation", key)
	if err := st.NextErr(); err != nil {
		return "", err
	}
	return st.offerNames[key], nil
}

func (st *mockState) ImportRemoteEntity(entity names.Tag, token string) error {
	st.MethodCall(st, "ImportRemoteEntity", entity, token)
	if err := st.NextErr(); err != nil {
		return err
	}
	if _, ok := st.remoteEntities[entity]; ok {
		return errors.AlreadyExistsf(entity.Id())
	}
	st.remoteEntities[entity] = token
	return nil
}

func (st *mockState) ExportLocalEntity(entity names.Tag) (string, error) {
	st.MethodCall(st, "ExportLocalEntity", entity)
	if err := st.NextErr(); err != nil {
		return "", err
	}
	if token, ok := st.remoteEntities[entity]; ok {
		return token, errors.AlreadyExistsf(entity.Id())
	}
	token := "token-" + entity.Id()
	st.remoteEntities[entity] = token
	return token, nil
}

func (st *mockState) GetRemoteEntity(token string) (names.Tag, error) {
	st.MethodCall(st, "GetRemoteEntity", token)
	if err := st.NextErr(); err != nil {
		return nil, err
	}
	for e, t := range st.remoteEntities {
		if t == token {
			return e, nil
		}
	}
	return nil, errors.NotFoundf("token %v", token)
}

func (st *mockState) GetToken(entity names.Tag) (string, error) {
	st.MethodCall(st, "GetToken", entity)
	if err := st.NextErr(); err != nil {
		return "", err
	}
	for e, t := range st.remoteEntities {
		if e.Id() == entity.Id() {
			return t, nil
		}
	}
	return "", errors.NotFoundf("entity %v", entity)
}

func (st *mockState) KeyRelation(key string) (commoncrossmodel.Relation, error) {
	st.MethodCall(st, "KeyRelation", key)
	if err := st.NextErr(); err != nil {
		return nil, err
	}
	r, ok := st.relations[key]
	if !ok {
		return nil, errors.NotFoundf("relation %q", key)
	}
	return r, nil
}

func (st *mockState) IsMigrationActive() (bool, error) {
	st.MethodCall(st, "IsMigrationActive")
	if err := st.NextErr(); err != nil {
		return false, err
	}
	return st.migrationActive, nil
}

func (st *mockState) RemoteApplication(id string) (commoncrossmodel.RemoteApplication, error) {
	st.MethodCall(st, "RemoteApplication", id)
	if err := st.NextErr(); err != nil {
		return nil, err
	}
	a, ok := st.remoteApplications[id]
	if !ok {
		return nil, errors.NotFoundf("remote application %q", id)
	}
	return a, nil
}

func (st *mockState) Application(id string) (commoncrossmodel.Application, error) {
	st.MethodCall(st, "Application", id)
	if err := st.NextErr(); err != nil {
		return nil, err
	}
	a, ok := st.applications[id]
	if !ok {
		return nil, errors.NotFoundf("application %q", id)
	}
	return a, nil
}

type mockFirewallState struct {
	firewall.State
}

type mockWatcher struct {
	mu      sync.Mutex
	stopped chan struct{}
}

func (w *mockWatcher) Kill() {
	w.mu.Lock()
	defer w.mu.Unlock()
	if !w.Stopped() {
		close(w.stopped)
	}
}

func (w *mockWatcher) Stop() error {
	return nil
}

func (w *mockWatcher) Wait() error {
	<-w.stopped
	return nil
}

func (w *mockWatcher) Err() error {
	return nil
}

func (w *mockWatcher) Stopped() bool {
	select {
	case <-w.stopped:
		return true
	default:
		return false
	}
}

type mockRelationStatusWatcher struct {
	*mockWatcher
	changes chan []string
}

func (w *mockRelationStatusWatcher) Changes() <-chan []string {
	return w.changes
}

type mockOfferStatusWatcher struct {
	*mockWatcher
	offerUUID string
	offerName string
	changes   chan struct{}
}

func (w *mockOfferStatusWatcher) Changes() <-chan struct{} {
	return w.changes
}

func (w *mockOfferStatusWatcher) OfferUUID() string {
	return w.offerUUID
}

func (w *mockOfferStatusWatcher) OfferName() string {
	return w.offerName
}

type mockModel struct {
}

func (m *mockModel) Name() string {
	return "prod"
}

func (m *mockModel) Owner() names.UserTag {
	return names.NewUserTag("fred")
}

type mockRelation struct {
	commoncrossmodel.Relation
	testing.Stub
	id              int
	key             string
	suspended       bool
	suspendedReason string
	status          status.Status
	message         string
	units           map[string]commoncrossmodel.RelationUnit
	endpoints       []state.Endpoint
	watchers        map[string]*mockUnitsWatcher
	appSettings     map[string]map[string]interface{}
}

func newMockRelation(id int) *mockRelation {
	return &mockRelation{
		id:          id,
		units:       make(map[string]commoncrossmodel.RelationUnit),
		watchers:    make(map[string]*mockUnitsWatcher),
		appSettings: make(map[string]map[string]interface{}),
	}
}

func (r *mockRelation) Id() int {
	r.MethodCall(r, "Id")
	return r.id
}

func (r *mockRelation) Tag() names.Tag {
	r.MethodCall(r, "Tag")
	return names.NewRelationTag(r.key)
}

func (r *mockRelation) Destroy() error {
	r.MethodCall(r, "Destroy")
	return r.NextErr()
}

func (r *mockRelation) Life() state.Life {
	return state.Alive
}

func (r *mockRelation) UnitCount() int {
	return 666
}

func (r *mockRelation) SetStatus(statusInfo status.StatusInfo) error {
	r.MethodCall(r, "SetStatus")
	r.status = statusInfo.Status
	r.message = statusInfo.Message
	return nil
}

func (r *mockRelation) SetSuspended(suspended bool, reason string) error {
	r.MethodCall(r, "SetSuspended")
	r.suspended = suspended
	r.suspendedReason = reason
	return nil
}

func (r *mockRelation) Suspended() bool {
	r.MethodCall(r, "Suspended")
	return r.suspended
}

func (r *mockRelation) SuspendedReason() string {
	r.MethodCall(r, "SuspendedReason")
	return r.suspendedReason
}

func (r *mockRelation) RemoteUnit(unitId string) (commoncrossmodel.RelationUnit, error) {
	r.MethodCall(r, "RemoteUnit", unitId)
	if err := r.NextErr(); err != nil {
		return nil, err
	}
	u, ok := r.units[unitId]
	if !ok {
		return nil, errors.NotFoundf("unit %q", unitId)
	}
	return u, nil
}

func (r *mockRelation) AllRemoteUnits(appName string) ([]commoncrossmodel.RelationUnit, error) {
	r.MethodCall(r, "AllRemoteUnits", appName)
	if err := r.NextErr(); err != nil {
		return nil, err
	}
	var result []commoncrossmodel.RelationUnit
	for _, ru := range r.units {
		result = append(result, ru)
	}
	return result, nil
}

func (r *mockRelation) Unit(unitId string) (commoncrossmodel.RelationUnit, error) {
	r.MethodCall(r, "Unit", unitId)
	if err := r.NextErr(); err != nil {
		return nil, err
	}
	u, ok := r.units[unitId]
	if !ok {
		return nil, errors.NotFoundf("unit %q", unitId)
	}
	return u, nil
}

func (r *mockRelation) ReplaceApplicationSettings(appName string, values map[string]interface{}) error {
	r.MethodCall(r, "ReplaceApplicationSettings", appName, values)
	return r.NextErr()
}

func (r *mockRelation) Endpoints() []state.Endpoint {
	r.MethodCall(r, "Endpoints")
	return r.endpoints
}

func (r *mockRelation) WatchUnits(appName string) (state.RelationUnitsWatcher, error) {
	r.MethodCall(r, "WatchUnits", appName)
	if err := r.NextErr(); err != nil {
		return nil, err
	}
	w, found := r.watchers[appName]
	if !found {
		return nil, errors.NotFoundf("fake watcher for %q units", appName)
	}
	return w, nil
}

func (r *mockRelation) ApplicationSettings(appName string) (map[string]interface{}, error) {
	r.MethodCall(r, "ApplicationSettings", appName)
	if err := r.NextErr(); err != nil {
		return nil, err
	}
	settings, found := r.appSettings[appName]
	if !found {
		return nil, errors.NotFoundf("fake settings for %q", appName)
	}
	return settings, nil
}

type mockRemoteApplication struct {
	commoncrossmodel.RemoteApplication
	testing.Stub
	consumerproxy   bool
	sourceModelUUID string
}

func (r *mockRemoteApplication) IsConsumerProxy() bool {
	r.MethodCall(r, "IsConsumerProxy")
	return r.consumerproxy
}

func (r *mockRemoteApplication) Destroy() error {
	r.MethodCall(r, "Destroy")
	return r.NextErr()
}

type mockApplication struct {
	commoncrossmodel.Application
	name string
	testing.Stub
	life state.Life
	eps  []state.Endpoint
}

func (a *mockApplication) Name() string {
	a.MethodCall(a, "Name")
	return a.name
}

func (a *mockApplication) Endpoints() ([]state.Endpoint, error) {
	a.MethodCall(a, "Endpoints")
	return a.eps, nil
}

func (a *mockApplication) Life() state.Life {
	a.MethodCall(a, "Life")
	return a.life
}

func (a *mockApplication) Status() (status.StatusInfo, error) {
	a.MethodCall(a, "Status")
	return status.StatusInfo{Status: status.Terminated}, nil
}

type mockOfferConnection struct {
	crossmodelrelations.OfferConnection
	sourcemodelUUID string
	relationId      int
	relationKey     string
	username        string
	offerUUID       string
}

func (m *mockOfferConnection) OfferUUID() string {
	return m.offerUUID
}

type mockRelationUnit struct {
	commoncrossmodel.RelationUnit
	testing.Stub
	inScope  bool
	settings map[string]interface{}
}

func newMockRelationUnit() *mockRelationUnit {
	return &mockRelationUnit{
		settings: make(map[string]interface{}),
	}
}

func (u *mockRelationUnit) InScope() (bool, error) {
	u.MethodCall(u, "InScope")
	return u.inScope, u.NextErr()
}

func (u *mockRelationUnit) LeaveScope() error {
	u.MethodCall(u, "LeaveScope")
	if err := u.NextErr(); err != nil {
		return err
	}
	u.inScope = false
	return nil
}

func (u *mockRelationUnit) EnterScope(settings map[string]interface{}) error {
	u.MethodCall(u, "EnterScope", settings)
	if err := u.NextErr(); err != nil {
		return err
	}
	u.inScope = true
	u.settings = make(map[string]interface{})
	for k, v := range settings {
		u.settings[k] = v
	}
	return nil
}

func (u *mockRelationUnit) Settings() (map[string]interface{}, error) {
	u.MethodCall(u, "Settings")
	return u.settings, u.NextErr()
}

func (u *mockRelationUnit) ReplaceSettings(settings map[string]interface{}) error {
	u.MethodCall(u, "ReplaceSettings", settings)
	if err := u.NextErr(); err != nil {
		return err
	}
	u.settings = make(map[string]interface{})
	for k, v := range settings {
		u.settings[k] = v
	}
	return nil
}

type mockAuthorizer struct{}

func (mockAuthorizer) AuthorizeOps(ctx context.Context, authorizedOp bakery.Op, queryOps []bakery.Op) ([]bool, []checkers.Caveat, error) {
	allowed := make([]bool, len(queryOps))
	for i := range allowed {
		allowed[i] = queryOps[i].Action == "consume" || queryOps[i].Action == "relate"
	}
	return allowed, nil, nil
}

type mockVerifier struct {
	ops []bakery.Op
}

func (m mockVerifier) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) ([]bakery.Op, []string, error) {
	declared := checkers.InferDeclared(charmstore.MacaroonNamespace, ms)
	var conditions []string
	for k, v := range declared {
		conditions = append(conditions, fmt.Sprintf("declared %v %v", k, v))
	}
	return m.ops, conditions, nil
}

type mockBakeryService struct {
	testing.Stub
	authentication.ExpirableStorageBakery
	ops []bakery.Op
}

func (s *mockBakeryService) NewMacaroon(ctx context.Context, version bakery.Version, caveats []checkers.Caveat, ops ...bakery.Op) (*bakery.Macaroon, error) {
	s.MethodCall(s, "NewMacaroon", caveats)
	mac, err := macaroon.New(nil, []byte("id"), "", macaroon.LatestVersion)
	if err != nil {
		return nil, err
	}
	for _, cav := range caveats {
		if err := mac.AddFirstPartyCaveat([]byte(cav.Condition)); err != nil {
			return nil, err
		}
	}
	s.ops = ops
	return bakery.NewLegacyMacaroon(mac)
}

func (s *mockBakeryService) Auth(mss ...macaroon.Slice) *bakery.AuthChecker {
	s.MethodCall(s, "Auth", mss)
	checker := bakery.NewChecker(bakery.CheckerParams{
		OpsAuthorizer:    mockAuthorizer{},
		MacaroonVerifier: mockVerifier{s.ops},
	})
	return checker.Auth(mss...)
}

func (s *mockBakeryService) ExpireStorageAfter(when time.Duration) (authentication.ExpirableStorageBakery, error) {
	s.MethodCall(s, "ExpireStorageAfter", when)
	return s, nil
}

type mockUnitsWatcher struct {
	*mockWatcher
	changes chan watcher.RelationUnitsChange
}

func (w *mockUnitsWatcher) Changes() watcher.RelationUnitsChannel {
	return w.changes
}
