//go:build windows && unit
// +build windows,unit

// Copyright 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 app

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/data"
	mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks"
	mock_engine "github.com/aws/amazon-ecs-agent/agent/engine/mocks"
	"github.com/aws/amazon-ecs-agent/agent/sighandlers"
	"github.com/aws/amazon-ecs-agent/agent/sighandlers/exitcodes"
	statemanager_mocks "github.com/aws/amazon-ecs-agent/agent/statemanager/mocks"
	"github.com/aws/amazon-ecs-agent/ecs-agent/ec2"
	"github.com/aws/amazon-ecs-agent/ecs-agent/eventstream"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	"golang.org/x/sys/windows/svc"
)

type mockAgent struct {
	startFunc          func() int
	terminationHandler sighandlers.TerminationHandler
}

func (m *mockAgent) start() int {
	return m.startFunc()
}
func (m *mockAgent) setTerminationHandler(handler sighandlers.TerminationHandler) {
	m.terminationHandler = handler
}
func (m *mockAgent) printECSAttributes() int   { return 0 }
func (m *mockAgent) startWindowsService() int  { return 0 }
func (m *mockAgent) getConfig() *config.Config { return &config.Config{} }

func TestHandler_RunAgent_StartExitImmediately(t *testing.T) {
	// register some mocks, but nothing should get called on any of them
	ctrl := gomock.NewController(t)
	_ = statemanager_mocks.NewMockStateManager(ctrl)
	_ = mock_engine.NewMockTaskEngine(ctrl)
	defer ctrl.Finish()

	wg := sync.WaitGroup{}
	wg.Add(1)
	startFunc := func() int {
		// startFunc doesn't block, nothing is called
		wg.Done()
		return 0
	}
	agent := &mockAgent{startFunc: startFunc}
	handler := &handler{agent}
	go handler.runAgent(context.TODO())
	wg.Wait()
	assert.NotNil(t, agent.terminationHandler)
}

func TestHandler_RunAgent_NoSaveWithNoTerminationHandler(t *testing.T) {
	// register some mocks, but nothing should get called on any of them
	ctrl := gomock.NewController(t)
	_ = statemanager_mocks.NewMockStateManager(ctrl)
	_ = mock_engine.NewMockTaskEngine(ctrl)
	defer ctrl.Finish()

	done := make(chan struct{})
	startFunc := func() int {
		<-done // block until after the test ends so that we can test that runAgent returns when cancelled
		return 0
	}
	agent := &mockAgent{startFunc: startFunc}
	handler := &handler{agent}
	ctx, cancel := context.WithCancel(context.TODO())
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		handler.runAgent(ctx)
		wg.Done()
	}()
	cancel()
	wg.Wait()
	assert.NotNil(t, agent.terminationHandler)
}

func TestHandler_RunAgent_ForceSaveWithTerminationHandler(t *testing.T) {
	ctrl := gomock.NewController(t)
	taskEngine := mock_engine.NewMockTaskEngine(ctrl)
	taskEngineState := mock_dockerstate.NewMockTaskEngineState(ctrl)
	defer ctrl.Finish()
	dataClient := data.NewNoopClient()

	taskEngine.EXPECT().Disable()
	taskEngineState.EXPECT().AllTasks().Return(nil)
	taskEngineState.EXPECT().AllImageStates().Return(nil)
	taskEngineState.EXPECT().AllENIAttachments().Return(nil)
	taskEngineState.EXPECT().GetAllContainerIDs().Return([]string{"test-container"})
	taskEngineState.EXPECT().ContainerByID("test-container").Return(nil, false)

	agent := &mockAgent{}

	ctx, cancel := context.WithCancel(context.TODO())
	done := make(chan struct{})
	defer func() { done <- struct{}{} }()
	startFunc := func() int {
		go agent.terminationHandler(taskEngineState, dataClient, taskEngine, cancel)
		<-done // block until after the test ends so that we can test that runAgent returns when cancelled
		return 0
	}
	agent.startFunc = startFunc
	handler := &handler{agent}
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		handler.runAgent(ctx)
		wg.Done()
	}()
	time.Sleep(time.Second) // give startFunc enough time to actually call the termination handler
	cancel()
	wg.Wait()
}

func TestHandler_HandleWindowsRequests_StopService(t *testing.T) {
	requests := make(chan svc.ChangeRequest)
	responses := make(chan svc.Status)

	handler := &handler{}
	wg := sync.WaitGroup{}
	wg.Add(2)

	go func() {
		handler.handleWindowsRequests(context.TODO(), requests, responses)
		wg.Done()
	}()

	go func() {
		resp := <-responses
		assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately")
		resp = <-responses
		assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending")
		assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown")
		requests <- svc.ChangeRequest{Cmd: svc.Interrogate, CurrentStatus: svc.Status{State: svc.Running}}
		resp = <-responses
		assert.Equal(t, svc.Running, resp.State, "Send Running after Interrogate")
		requests <- svc.ChangeRequest{Cmd: svc.Stop}
		resp = <-responses
		assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after Stop")
		wg.Done()
	}()

	wg.Wait()
}

func TestHandler_HandleWindowsRequests_Cancel(t *testing.T) {
	requests := make(chan svc.ChangeRequest)
	responses := make(chan svc.Status)

	handler := &handler{}
	ctx, cancel := context.WithCancel(context.TODO())
	wg := sync.WaitGroup{}
	wg.Add(2)

	go func() {
		handler.handleWindowsRequests(ctx, requests, responses)
		wg.Done()
	}()

	go func() {
		resp := <-responses
		assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately")
		resp = <-responses
		assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending")
		assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown")
		requests <- svc.ChangeRequest{Cmd: svc.Interrogate, CurrentStatus: svc.Status{State: svc.Running}}
		resp = <-responses
		assert.Equal(t, svc.Running, resp.State, "Send Running after Interrogate")
		cancel()
		resp = <-responses
		assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after Cancel")
		wg.Done()
	}()

	wg.Wait()
}

func TestHandler_Execute_WindowsStops(t *testing.T) {
	ctrl := gomock.NewController(t)
	taskEngine := mock_engine.NewMockTaskEngine(ctrl)
	taskEngineState := mock_dockerstate.NewMockTaskEngineState(ctrl)
	defer ctrl.Finish()
	dataClient := data.NewNoopClient()

	taskEngine.EXPECT().Disable()
	taskEngineState.EXPECT().AllTasks().Return(nil)
	taskEngineState.EXPECT().AllImageStates().Return(nil)
	taskEngineState.EXPECT().AllENIAttachments().Return(nil)
	taskEngineState.EXPECT().GetAllContainerIDs().Return([]string{"test-container"})
	taskEngineState.EXPECT().ContainerByID("test-container").Return(nil, false)

	agent := &mockAgent{}

	_, cancel := context.WithCancel(context.TODO())
	done := make(chan struct{})
	defer func() { done <- struct{}{} }()
	startFunc := func() int {
		go agent.terminationHandler(taskEngineState, dataClient, taskEngine, cancel)
		<-done // block until after the test ends so that we can test that Execute returns when Stopped
		return 0
	}
	agent.startFunc = startFunc
	handler := &handler{agent}
	requests := make(chan svc.ChangeRequest)
	responses := make(chan svc.Status)

	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		handler.Execute(nil, requests, responses)
		wg.Done()
	}()

	go func() {
		resp := <-responses
		assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately")
		resp = <-responses
		assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending")
		assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown")
		time.Sleep(time.Second) // let it run for a second
		requests <- svc.ChangeRequest{Cmd: svc.Shutdown}
		resp = <-responses
		assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after Shutdown")
		wg.Done()
	}()

	wg.Wait()
}

func TestHandler_Execute_AgentStops(t *testing.T) {
	agent := &mockAgent{}

	ctx, cancel := context.WithCancel(context.TODO())
	startFunc := func() int {
		<-ctx.Done()
		return 0
	}
	agent.startFunc = startFunc
	handler := &handler{agent}
	requests := make(chan svc.ChangeRequest)
	responses := make(chan svc.Status)

	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		handler.Execute(nil, requests, responses)
		wg.Done()
	}()

	go func() {
		resp := <-responses
		assert.Equal(t, svc.StartPending, resp.State, "Send StartPending immediately")
		resp = <-responses
		assert.Equal(t, svc.Running, resp.State, "Send Running after StartPending")
		assert.Equal(t, svc.AcceptStop|svc.AcceptShutdown, resp.Accepts, "Accept stop & shutdown")
		time.Sleep(time.Second) // let it run for a second
		cancel()
		resp = <-responses
		assert.Equal(t, svc.StopPending, resp.State, "Send StopPending after agent goroutine stops")
		wg.Done()
	}()

	wg.Wait()
}

func TestDoStartTaskLimitsFail(t *testing.T) {
	ctrl, credentialsManager, state, imageManager, client,
		dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, _ := setup(t)
	defer ctrl.Finish()

	cfg := getTestConfig()
	cfg.Checkpoint = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}
	cfg.TaskCPUMemLimit.Value = config.ExplicitlyEnabled
	ctx, cancel := context.WithCancel(context.TODO())
	// Cancel the context to cancel async routines
	defer cancel()
	agent := &ecsAgent{
		ctx:                   ctx,
		cfg:                   &cfg,
		dockerClient:          dockerClient,
		stateManagerFactory:   stateManagerFactory,
		saveableOptionFactory: saveableOptionFactory,
		ec2MetadataClient:     ec2.NewBlackholeEC2MetadataClient(),
	}

	dockerClient.EXPECT().SupportedVersions().Return(apiVersions)

	exitCode := agent.doStart(eventstream.NewEventStream("events", ctx),
		credentialsManager, state, imageManager, client, execCmdMgr)
	assert.Equal(t, exitcodes.ExitTerminal, exitCode)
}
