package dockeracquisition

import (
	"context"
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"runtime"
	"strings"
	"testing"
	"time"

	dockerContainer "github.com/moby/moby/api/types/container"
	dockerTypesEvents "github.com/moby/moby/api/types/events"
	dockerTypesSwarm "github.com/moby/moby/api/types/swarm"
	"github.com/moby/moby/api/types/system"
	"github.com/moby/moby/client"
	log "github.com/sirupsen/logrus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/tomb.v2"

	"github.com/crowdsecurity/go-cs-lib/cstest"

	"github.com/crowdsecurity/crowdsec/pkg/metrics"
	"github.com/crowdsecurity/crowdsec/pkg/pipeline"
)

const testContainerName = "docker_test"
const testServiceName = "test_service"

func TestConfigureDSN(t *testing.T) {
	log.Infof("Test 'TestConfigureDSN'")

	ctx := t.Context()

	var dockerHost string

	if runtime.GOOS == "windows" {
		dockerHost = "npipe:////./pipe/docker_engine"
	} else {
		dockerHost = "unix:///var/run/podman/podman.sock"
	}

	tests := []struct {
		name        string
		dsn         string
		expectedErr string
	}{
		{
			name:        "invalid DSN",
			dsn:         "asdfasdf",
			expectedErr: "invalid DSN asdfasdf for docker source, must start with docker://",
		},
		{
			name:        "invalid DSN scheme",
			dsn:         "asd://",
			expectedErr: "invalid DSN asd:// for docker source, must start with docker://",
		},
		{
			name:        "empty DSN",
			dsn:         "docker://",
			expectedErr: "empty docker:// DSN",
		},
		{
			name:        "DSN ok with log_level",
			dsn:         "docker://test_docker?log_level=warn",
			expectedErr: "",
		},
		{
			name:        "DSN invalid log_level",
			dsn:         "docker://test_docker?log_level=foobar",
			expectedErr: "unknown level foobar: not a valid logrus Level:",
		},
		{
			name:        "DSN ok with multiple parameters",
			dsn:         "docker://test_docker?since=42min&docker_host=" + dockerHost,
			expectedErr: "",
		},
	}
	subLogger := log.WithField("type", ModuleName)

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			f := Source{}
			err := f.ConfigureByDSN(ctx, test.dsn, map[string]string{"type": "testtype"}, subLogger, "")
			cstest.AssertErrorContains(t, err, test.expectedErr)
		})
	}
}

type mockDockerCli struct {
	client.Client
	services       []dockerTypesSwarm.Service
}

// Simplified Info method - just return basic info without complex types
func (*mockDockerCli) Info(_ context.Context, _ client.InfoOptions) (client.SystemInfoResult, error) {
	// For testing purposes, we'll set the swarm info based on our mock flag
	// The exact type matching can be handled in integration tests
	return client.SystemInfoResult{
		Info: system.Info{},
	}, nil
}

func (cli *mockDockerCli) ServiceList(_ context.Context, _ client.ServiceListOptions) (client.ServiceListResult, error) {
	items := cli.services

	if items == nil {
		defaultTestService :=  dockerTypesSwarm.Service{
			ID: "service123",
			Spec: dockerTypesSwarm.ServiceSpec{
				Annotations: dockerTypesSwarm.Annotations{
					Name: testServiceName,
					Labels: map[string]string{
						"service.type": "web",
					},
				},
			},
		}
		items = []dockerTypesSwarm.Service{defaultTestService}
	}

	return client.ServiceListResult{
		Items: items,
	}, nil
}

func (*mockDockerCli) ServiceLogs(ctx context.Context, _ string, options client.ServiceLogsOptions) (client.ServiceLogsResult, error) {
	// Return test data - behavior depends on whether this is streaming or oneshot
	data := []string{"service\n", "log\n", "test\n"}
	ret := ""

	for _, line := range data {
		startLineByte := make([]byte, 8)
		binary.LittleEndian.PutUint32(startLineByte, 1) // stdout stream
		binary.BigEndian.PutUint32(startLineByte[4:], uint32(len(line)))
		ret += fmt.Sprintf("%s%s", startLineByte, line)
	}

	if !options.Follow {
		// OneShot mode: return all data and close immediately
		return io.NopCloser(strings.NewReader(ret)), nil
	}

	// Streaming mode: send data then block to simulate a live service
	// This prevents infinite retry loops in streaming tests
	reader, writer := io.Pipe()

	go func() {
		defer writer.Close()
		// Write the test data
		if _, err := writer.Write([]byte(ret)); err != nil {
			return // Context likely canceled
		}
		// Then block to simulate a continuous connection
		<-ctx.Done()
	}()

	return reader, nil
}

func TestStreamingAcquisition(t *testing.T) {
	ctx := t.Context()

	log.SetOutput(os.Stdout)
	log.SetLevel(log.InfoLevel)
	log.Info("Test 'TestStreamingAcquisition'")

	tests := []struct {
		name           string
		config         string
		expectedErr    string
		expectedLines  int
		logType        string
		isSwarmManager bool
	}{
		{
			name: "container acquisition",
			config: `
source: docker
mode: cat
container_name:
 - docker_test`,
			expectedErr:    "",
			expectedLines:  3,
			logType:        "test",
			isSwarmManager: false,
		},
		{
			name: "container regexp acquisition",
			config: `
source: docker
mode: cat
container_name_regexp:
 - docker_*`,
			expectedErr:    "",
			expectedLines:  3,
			logType:        "test",
			isSwarmManager: false,
		},
		{
			name: "service acquisition",
			config: `
source: docker
mode: cat
service_name:
 - test_service`,
			expectedErr:    "",
			expectedLines:  3,
			logType:        "test",
			isSwarmManager: true,
		},
		{
			name: "service regexp acquisition",
			config: `
source: docker
mode: cat
service_name_regexp:
 - test_*`,
			expectedErr:    "",
			expectedLines:  3,
			logType:        "test",
			isSwarmManager: true,
		},
	}

	for _, ts := range tests {
		t.Run(ts.name, func(t *testing.T) {
			subLogger := log.WithField("type", ModuleName)

			dockerTomb := tomb.Tomb{}
			out := make(chan pipeline.Event)
			dockerSource := Source{}
			err := dockerSource.Configure(ctx, []byte(ts.config), subLogger, metrics.AcquisitionMetricsLevelNone)
			cstest.AssertErrorContains(t, err, ts.expectedErr)

			if ts.expectedErr != "" {
				return
			}

			dockerSource.Client = &mockDockerCli{}

			// Manually set swarm manager flag for testing since Info() mock is simplified
			dockerSource.isSwarmManager = ts.isSwarmManager
			actualLines := 0
			streamTomb := tomb.Tomb{}
			streamTomb.Go(func() error {
				return dockerSource.StreamingAcquisition(ctx, out, &dockerTomb)
			})

			require.Eventually(t, func() bool {
				select {
				case <-out:
					actualLines++
				default:
				}

				return actualLines >= ts.expectedLines
			}, 5*time.Second, 100*time.Millisecond, "did not receive expected log lines")

			dockerSource.t.Kill(nil)

			err = streamTomb.Wait()
			require.NoError(t, err)

			if ts.expectedLines != 0 {
				assert.Equal(t, ts.expectedLines, actualLines)
			}
		})
	}
}

func TestServiceEvaluation(t *testing.T) {
	log.Infof("Test 'TestServiceEvaluation'")

	ctx := t.Context()

	tests := []struct {
		name           string
		config         string
		service        dockerTypesSwarm.Service
		expectedMatch  bool
		isSwarmManager bool
	}{
		{
			name: "service name match",
			config: `
source: docker
mode: cat
service_name:
 - web-service`,
			service: dockerTypesSwarm.Service{
				ID: "svc1",
				Spec: dockerTypesSwarm.ServiceSpec{
					Annotations: dockerTypesSwarm.Annotations{
						Name: "web-service",
					},
				},
			},
			expectedMatch:  true,
			isSwarmManager: true,
		},
		{
			name: "service name no match",
			config: `
source: docker
mode: cat
service_name:
 - api-service`,
			service: dockerTypesSwarm.Service{
				ID: "svc1",
				Spec: dockerTypesSwarm.ServiceSpec{
					Annotations: dockerTypesSwarm.Annotations{
						Name: "web-service",
					},
				},
			},
			expectedMatch:  false,
			isSwarmManager: true,
		},
		{
			name: "service ID match",
			config: `
source: docker
mode: cat
service_id:
 - svc123`,
			service: dockerTypesSwarm.Service{
				ID: "svc123",
				Spec: dockerTypesSwarm.ServiceSpec{
					Annotations: dockerTypesSwarm.Annotations{
						Name: "web-service",
					},
				},
			},
			expectedMatch:  true,
			isSwarmManager: true,
		},
		{
			name: "service name regexp match",
			config: `
source: docker
mode: cat
service_name_regexp:
 - web-.*`,
			service: dockerTypesSwarm.Service{
				ID: "svc1",
				Spec: dockerTypesSwarm.ServiceSpec{
					Annotations: dockerTypesSwarm.Annotations{
						Name: "web-service",
					},
				},
			},
			expectedMatch:  true,
			isSwarmManager: true,
		},
		{
			name: "service labels match",
			config: `
source: docker
mode: cat
use_service_labels: true`,
			service: dockerTypesSwarm.Service{
				ID: "svc1",
				Spec: dockerTypesSwarm.ServiceSpec{
					Annotations: dockerTypesSwarm.Annotations{
						Name: "web-service",
						Labels: map[string]string{
							"crowdsec.enable":      "true",
							"crowdsec.labels.type": "nginx",
						},
					},
				},
			},
			expectedMatch:  true,
			isSwarmManager: true,
		},
	}

	subLogger := log.WithField("type", ModuleName)

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			f := Source{}
			err := f.Configure(ctx, []byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
			require.NoError(t, err)

			f.Client = &mockDockerCli{}

			// Manually set swarm manager flag for testing
			f.isSwarmManager = test.isSwarmManager

			result := f.EvalService(ctx, test.service)
			if test.expectedMatch {
				assert.NotNil(t, result, "Expected service to match but got nil")

				if result != nil {
					assert.Equal(t, test.service.ID, result.ID)
					assert.Equal(t, test.service.Spec.Name, result.Name)
				}
			} else {
				assert.Nil(t, result, "Expected service not to match but got result")
			}
		})
	}
}

func TestSwarmManagerDetection(t *testing.T) {
	log.Infof("Test 'TestSwarmManagerDetection'")

	ctx := t.Context()

	tests := []struct {
		name           string
		config         string
		isSwarmManager bool
		expectedSwarm  bool
		expectedWarn   bool
	}{
		{
			name: "swarm manager with service config",
			config: `
source: docker
mode: cat
service_name:
 - web-service`,
			isSwarmManager: true,
			expectedSwarm:  true,
			expectedWarn:   false,
		},
		{
			name: "swarm manager without service config",
			config: `
source: docker
mode: cat
container_name:
 - test-container`,
			isSwarmManager: true,
			expectedSwarm:  false,
			expectedWarn:   true,
		},
		{
			name: "non-swarm with service config",
			config: `
source: docker
mode: cat
service_name:
 - web-service`,
			isSwarmManager: false,
			expectedSwarm:  false,
			expectedWarn:   false,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			subLogger := log.WithField("type", ModuleName)
			f := Source{
				Client: &mockDockerCli{},
			}

			err := f.Configure(ctx, []byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
			require.NoError(t, err)

			// For this test, we manually set the expected behavior since Info() is simplified
			f.isSwarmManager = test.expectedSwarm

			assert.Equal(t, test.expectedSwarm, f.isSwarmManager)
		})
	}
}

func (*mockDockerCli) ContainerList(_ context.Context, _ client.ContainerListOptions) (client.ContainerListResult, error) {
	// Always return test container for the mock
	result := client.ContainerListResult{}
	result.Items = []dockerContainer.Summary{
		{
			ID:    "12456",
			Names: []string{testContainerName},
		},
	}

	return result, nil
}

func (*mockDockerCli) ContainerLogs(ctx context.Context, _ string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
	// Return test data - behavior depends on whether this is streaming or oneshot
	data := []string{"docker\n", "test\n", "1234\n"}
	ret := ""

	for _, line := range data {
		startLineByte := make([]byte, 8)
		binary.LittleEndian.PutUint32(startLineByte, 1) // stdout stream
		binary.BigEndian.PutUint32(startLineByte[4:], uint32(len(line)))
		ret += fmt.Sprintf("%s%s", startLineByte, line)
	}

	if !options.Follow {
		// OneShot mode: return all data and close immediately
		return io.NopCloser(strings.NewReader(ret)), nil
	}

	// Streaming mode: send data then block to simulate a live container
	// This prevents infinite retry loops in streaming tests
	reader, writer := io.Pipe()

	go func() {
		defer writer.Close()
		// Write the test data
		if _, err := writer.Write([]byte(ret)); err != nil {
			return // Context likely canceled
		}
		// Then block to simulate a continuous connection
		<-ctx.Done()
	}()

	return reader, nil
}

func (*mockDockerCli) ContainerInspect(_ context.Context, _ string, _ client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
	res := client.ContainerInspectResult{}
	res.Container = dockerContainer.InspectResponse{
		Config: &dockerContainer.Config{ Tty: false },
		State:  &dockerContainer.State{ Running: true }, // Mock container is running
	}

	return res, nil
}

func (*mockDockerCli) ServiceInspectWithRaw(_ context.Context, serviceID string, _ client.ServiceInspectOptions) (dockerTypesSwarm.Service, []byte, error) {
	// Return a mock service that exists
	service := dockerTypesSwarm.Service{
		ID: serviceID,
		Spec: dockerTypesSwarm.ServiceSpec{
			Annotations: dockerTypesSwarm.Annotations{
				Name: testServiceName,
			},
		},
	}

	return service, []byte("{}"), nil
}
// Since we are mocking the docker client, we return channels that will never be used
func (*mockDockerCli) Events(_ context.Context, _ client.EventsListOptions) client.EventsResult {
	eventsChan := make(chan dockerTypesEvents.Message)
	errChan := make(chan error)

	return client.EventsResult{
		Messages: eventsChan,
		Err: errChan,
	}
}

func TestOneShot(t *testing.T) {
	ctx := t.Context()

	log.Info("Test 'TestOneShot'")

	tests := []struct {
		dsn            string
		expectedErr    string
		expectedOutput string
		expectedLines  int
		logType        string
	}{
		{
			dsn:            "docker://non_exist_docker",
			expectedErr:    "no container found named: non_exist_docker, can't run one shot acquisition",
			expectedOutput: "",
			expectedLines:  0,
			logType:        "test",
		},
		{
			dsn:            "docker://" + testContainerName,
			expectedErr:    "",
			expectedOutput: "",
			expectedLines:  3,
			logType:        "test",
		},
	}

	for _, ts := range tests {
		t.Run(ts.dsn, func(t *testing.T) {
			subLogger := log.WithField("type", ModuleName)

			dockerClient := &Source{}
			labels := make(map[string]string)
			labels["type"] = ts.logType

			err := dockerClient.ConfigureByDSN(ctx, ts.dsn, labels, subLogger, "")
			require.NoError(t, err)

			dockerClient.Client = &mockDockerCli{}
			out := make(chan pipeline.Event, 100)

			tomb := tomb.Tomb{}

			err = dockerClient.OneShotAcquisition(ctx, out, &tomb)
			cstest.AssertErrorContains(t, err, ts.expectedErr)

			// else we do the check before actualLines is incremented ...
			if ts.expectedLines != 0 {
				assert.Len(t, out, ts.expectedLines)
			}
		})
	}
}

func TestParseLabels(t *testing.T) {
	tests := []struct {
		name     string
		labels   map[string]string
		expected map[string]any
	}{
		{
			name:     "bad label",
			labels:   map[string]string{"crowdsecfoo": "bar"},
			expected: map[string]any{},
		},
		{
			name:     "simple label",
			labels:   map[string]string{"crowdsec.bar": "baz"},
			expected: map[string]any{"bar": "baz"},
		},
		{
			name:     "multiple simple labels",
			labels:   map[string]string{"crowdsec.bar": "baz", "crowdsec.foo": "bar"},
			expected: map[string]any{"bar": "baz", "foo": "bar"},
		},
		{
			name:     "multiple simple labels 2",
			labels:   map[string]string{"crowdsec.bar": "baz", "bla": "foo"},
			expected: map[string]any{"bar": "baz"},
		},
		{
			name:     "end with dot",
			labels:   map[string]string{"crowdsec.bar.": "baz"},
			expected: map[string]any{},
		},
		{
			name:     "consecutive dots",
			labels:   map[string]string{"crowdsec......bar": "baz"},
			expected: map[string]any{},
		},
		{
			name:     "crowdsec labels",
			labels:   map[string]string{"crowdsec.labels.type": "nginx"},
			expected: map[string]any{"labels": map[string]any{"type": "nginx"}},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			labels := parseLabels(test.labels)
			assert.Equal(t, test.expected, labels)
		})
	}
}
