// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.
// Package tests represents stress and integration tests of the agent
package tests

import (
	"errors"
	"fmt"
	"os"
	"runtime/debug"
	"testing"
	"time"

	"github.com/aws/amazon-ssm-agent/agent/framework/coremodules"
	identity2 "github.com/aws/amazon-ssm-agent/common/identity/identity"
	"github.com/aws/amazon-ssm-agent/core/app/runtimeconfiginit"

	"github.com/aws/amazon-ssm-agent/agent/agent"
	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/context"
	"github.com/aws/amazon-ssm-agent/agent/contracts"
	"github.com/aws/amazon-ssm-agent/agent/framework/coremanager"
	"github.com/aws/amazon-ssm-agent/agent/health"
	"github.com/aws/amazon-ssm-agent/agent/hibernation"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/log/ssmlog"
	"github.com/aws/amazon-ssm-agent/common/identity"
	"github.com/aws/amazon-ssm-agent/internal/tests/testutils"
	"github.com/aws/aws-sdk-go/service/ssm"
	ssmsdkmock "github.com/aws/aws-sdk-go/service/ssm/ssmiface/mocks"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
)

type AgentHealthIntegrationTestSuite struct {
	suite.Suite
	context        context.T
	ssmAgent       agent.ISSMAgent
	ssmSdkMock     *ssmsdkmock.SSMAPI
	config         appconfig.SsmagentConfig
	ssmHealthCheck *health.HealthCheck
	log            log.T
}

func (suite *AgentHealthIntegrationTestSuite) SetupTest() {
	var err error
	var agentIdentity identity.IAgentIdentity
	log := ssmlog.SSMLogger(true)
	suite.log = log

	config, configerr := appconfig.Config(true)
	if configerr != nil {
		log.Debugf("appconfig could not be loaded - %v", configerr)
		return
	}
	config.Ssm.HealthFrequencyMinutes = 1
	suite.config = config
	// Add config into context
	identitySelector := identity2.NewDefaultAgentIdentitySelector(log)
	agentIdentity, err = identity2.NewAgentIdentity(log, &config, identitySelector)
	if err != nil {
		log.Debugf("unable to assume identity - %v", err)
		return
	}

	suite.context = context.Default(log, config, agentIdentity)

	rtci := runtimeconfiginit.New(log, agentIdentity)
	if err := rtci.Init(); err != nil {
		panic(fmt.Sprintf("Failed to initialize runtimeconfig: %v", err))
	}

	// Changing the minimum heathcheck frequency as 1 minute for testing
	suite.context.AppConstants().MinHealthFrequencyMinutes = 1

	// Use healthcheck service with mock ssm service injected.
	healthcheck, ssmsdkMock := testutils.NewHealthCheck(suite.context)
	suite.ssmHealthCheck = healthcheck
	suite.ssmSdkMock = ssmsdkMock

	hs := hibernation.NewHibernateMode(healthcheck, suite.context)
	assert.NotNil(suite.T(), hs)

	var modules []contracts.ICoreModuleWrapper
	modules = append(modules, coremodules.NewCoreModuleWrapper(log, healthcheck))

	// Only inject the healthcheck module into coremanager
	var cpm *coremanager.CoreManager
	if cpm, err = testutils.NewCoreManager(suite.context, &modules); err != nil {
		log.Errorf("error occurred when starting core manager: %v", err)
		return
	}
	log.Info("Finish Setting up Coremanager")
	// Create core ssm agent and set the coremanager & context into it.
	suite.ssmAgent = agent.NewSSMAgent(suite.context, healthcheck, hs)
	suite.ssmAgent.SetContext(suite.context)
	suite.ssmAgent.SetCoreManager(cpm)
}

func (suite *AgentHealthIntegrationTestSuite) TearDownSuite() {
	// Cleanup runtime config
	os.RemoveAll(appconfig.RuntimeConfigFolderPath)
}

func (suite *AgentHealthIntegrationTestSuite) TestHealthCheck() {
	suite.log.Info("Starting Test Health Check, it will take around 5-8 minutes.")
	// Setting the number of excepted scheduled health ping as 5
	designHealthPings := 5

	defer func() {
		// Logging the errors and handle agent panics
		if msg := recover(); msg != nil {
			suite.log.Errorf("Agent crashed with message %v!", msg)
			suite.log.Errorf("%s: %s", msg, debug.Stack())
		}
		suite.log.Flush()
		suite.log.Close()
	}()

	start := time.Now().UTC()
	// create a new channel for blocking the test.
	c := make(chan string)
	actualHealthPings := 0
	suite.ssmSdkMock.On("UpdateInstanceInformation", mock.AnythingOfType("*ssm.UpdateInstanceInformationInput")).Return(func(*ssm.UpdateInstanceInformationInput) *ssm.UpdateInstanceInformationOutput {
		// The time elapse since test start.
		elapsed := time.Since(start).Minutes()
		actualHealthPings++
		suite.log.Infof("Waiting for next health ping, elapsed time is %v minute \n", elapsed)
		if elapsed >= time.Duration(time.Duration(designHealthPings-1)*time.Minute).Minutes() {
			if actualHealthPings >= designHealthPings {
				c <- "Succeed"
				suite.T().Log("HealthCheck test succeed.")
			} else {
				// If the number of method calls didn't match expected value, assert the test failed
				c <- "Failed"
				suite.T().Error("HealthCheck test failed. Didn't get enough health pings in testing time interval")
			}
		}
		return &ssm.UpdateInstanceInformationOutput{}
	}, nil).Times(designHealthPings + 1)

	suite.ssmAgent.Start()
	<-c
	//Stop agent execution
	suite.ssmAgent.Stop()
}

func (suite *AgentHealthIntegrationTestSuite) TestHibernationCheck() {
	//Turn on the mock of GetAgentState, return Passive status and error.
	suite.T().Log("Starting Test Hibernation, it will take around 5 minutes.")
	exitHibernate := false
	defer func() {
		// Logging the errors and handle agent panics
		if msg := recover(); msg != nil {
			suite.log.Errorf("Agent crashed with message %v!", msg)
			suite.log.Errorf("%s: %s", msg, debug.Stack())
		}
		suite.log.Flush()
	}()

	suite.T().Log("Turn on Test Hibernations mocks")
	// Open the sdkmock for UpdateInstanceInformation once, agent state is Passive
	suite.ssmSdkMock.On("UpdateInstanceInformation", mock.AnythingOfType("*ssm.UpdateInstanceInformationInput")).Return(&ssm.UpdateInstanceInformationOutput{}, func(*ssm.UpdateInstanceInformationInput) error {
		// Turn on mock , Agent State will first return Passive for first time
		assert.Equal(suite.T(), exitHibernate, false)
		suite.log.Info("Waiting for next ping for hibernation, time interval is 5 Minutes \n")
		return errors.New("integration test is blocked in hibernation mode")
	}).Times(5)

	// Change sdkmock behavior, agent state back to Active
	suite.ssmSdkMock.On("UpdateInstanceInformation", mock.AnythingOfType("*ssm.UpdateInstanceInformationInput")).Return(&ssm.UpdateInstanceInformationOutput{}, func(*ssm.UpdateInstanceInformationInput) error {
		suite.T().Log("Hibernation module try to get agent state second time, return nil as error")
		return nil
	})

	suite.ssmAgent.Hibernate()
	exitHibernate = true
	suite.T().Log("Hibernation Test Succeed")
}

func TestAgentHealthTestSuite(t *testing.T) {
	suite.Run(t, new(AgentHealthIntegrationTestSuite))
}
