//go:build !windows

/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License 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 cio

import (
	"bytes"
	"context"
	"io"
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"testing"

	"github.com/containerd/fifo"
	"github.com/stretchr/testify/assert"
)

func TestOpenFifos(t *testing.T) {
	scenarios := []*FIFOSet{
		{
			Config: Config{
				Stdin:  "",
				Stdout: filepath.Join("This/does/not/exist", "test-stdout"),
				Stderr: filepath.Join("This/does/not/exist", "test-stderr"),
			},
		},
		{
			Config: Config{
				Stdin:  filepath.Join("This/does/not/exist", "test-stdin"),
				Stdout: "",
				Stderr: filepath.Join("This/does/not/exist", "test-stderr"),
			},
		},
		{
			Config: Config{
				Stdin:  "",
				Stdout: "",
				Stderr: filepath.Join("This/does/not/exist", "test-stderr"),
			},
		},
	}
	for _, scenario := range scenarios {
		_, err := openFifos(context.Background(), scenario)
		assert.Error(t, err, scenario)
	}
}

// TestOpenFifosWithTerminal tests openFifos should not open stderr if terminal
// is set.
func TestOpenFifosWithTerminal(t *testing.T) {
	ctx := t.Context()
	ioFifoDir := t.TempDir()

	cfg := Config{
		Stdout: filepath.Join(ioFifoDir, "test-stdout"),
		Stderr: filepath.Join(ioFifoDir, "test-stderr"),
	}

	// Without terminal, pipes.Stderr should not be nil
	{
		p, err := openFifos(ctx, NewFIFOSet(cfg, nil))
		if err != nil {
			t.Fatalf("unexpected error during openFifos: %v", err)
		}

		if p.Stderr == nil {
			t.Fatalf("unexpected empty stderr pipe")
		}
	}

	// With terminal, pipes.Stderr should be nil
	{
		cfg.Terminal = true
		p, err := openFifos(ctx, NewFIFOSet(cfg, nil))
		if err != nil {
			t.Fatalf("unexpected error during openFifos: %v", err)
		}

		if p.Stderr != nil {
			t.Fatalf("unexpected stderr pipe")
		}
	}
}

func assertHasPrefix(t *testing.T, s, prefix string) {
	t.Helper()
	if !strings.HasPrefix(s, prefix) {
		t.Fatalf("expected %s to start with %s", s, prefix)
	}
}

func TestNewFIFOSetInDir(t *testing.T) {
	root := t.TempDir()

	fifos, err := NewFIFOSetInDir(root, "theid", true)
	assert.NoError(t, err)

	dir := filepath.Dir(fifos.Stdin)
	assertHasPrefix(t, dir, root)
	expected := &FIFOSet{
		Config: Config{
			Stdin:    filepath.Join(dir, "theid-stdin"),
			Stdout:   filepath.Join(dir, "theid-stdout"),
			Stderr:   filepath.Join(dir, "theid-stderr"),
			Terminal: true,
		},
	}

	assert.Equal(t, fifos.Config, expected.Config)

	files, err := os.ReadDir(root)
	assert.NoError(t, err)
	assert.Len(t, files, 1)

	assert.Nil(t, fifos.Close())
	files, err = os.ReadDir(root)
	assert.NoError(t, err)
	assert.Len(t, files, 0)
}

func TestNewAttach(t *testing.T) {
	testCases := []struct {
		name                                          string
		expectedStdin, expectedStdout, expectedStderr string
	}{
		{
			name:           "attach to all streams (stdin, stdout, and stderr)",
			expectedStdin:  "this is the stdin",
			expectedStdout: "this is the stdout",
			expectedStderr: "this is the stderr",
		},
		{
			name:           "don't attach to stdin",
			expectedStdout: "this is the stdout",
			expectedStderr: "this is the stderr",
		},
		{
			name:           "don't attach to stdout",
			expectedStdin:  "this is the stdin",
			expectedStderr: "this is the stderr",
		},
		{
			name:           "don't attach to stderr",
			expectedStdin:  "this is the stdin",
			expectedStdout: "this is the stdout",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var (
				stdin  = bytes.NewBufferString(tc.expectedStdin)
				stdout = new(bytes.Buffer)
				stderr = new(bytes.Buffer)

				// The variables below have to be of the interface type (i.e., io.Reader/io.Writer)
				// instead of the concrete type (i.e., *bytes.Buffer) *before* being passed to NewAttach.
				// Otherwise, in NewAttach, the interface value won't be nil
				// (it's just that the concrete value inside the interface itself is nil. [1]),
				// which means that the corresponding FIFO path won't be set to be an empty string,
				// and that's not what we want.
				//
				// [1] https://go.dev/tour/methods/12
				stdinArg             io.Reader
				stdoutArg, stderrArg io.Writer
			)
			if tc.expectedStdin != "" {
				stdinArg = stdin
			}
			if tc.expectedStdout != "" {
				stdoutArg = stdout
			}
			if tc.expectedStderr != "" {
				stderrArg = stderr
			}

			attacher := NewAttach(WithStreams(stdinArg, stdoutArg, stderrArg))

			fifos, err := NewFIFOSetInDir("", "theid", false)
			assert.NoError(t, err)

			attachedFifos, err := attacher(fifos)
			assert.NoError(t, err)
			defer attachedFifos.Close()

			producers := setupFIFOProducers(t, attachedFifos.Config())
			initProducers(t, producers, tc.expectedStdout, tc.expectedStderr)

			var actualStdin []byte
			if producers.Stdin != nil {
				actualStdin, err = io.ReadAll(producers.Stdin)
				assert.NoError(t, err)
			}

			attachedFifos.Wait()
			attachedFifos.Cancel()
			assert.Nil(t, attachedFifos.Close())

			assert.Equal(t, tc.expectedStdout, stdout.String())
			assert.Equal(t, tc.expectedStderr, stderr.String())
			assert.Equal(t, tc.expectedStdin, string(actualStdin))
		})
	}
}

type producers struct {
	Stdin  io.ReadCloser
	Stdout io.WriteCloser
	Stderr io.WriteCloser
}

func setupFIFOProducers(t *testing.T, fifos Config) producers {
	var (
		err   error
		pipes producers
		ctx   = context.Background()
	)

	if fifos.Stdin != "" {
		pipes.Stdin, err = fifo.OpenFifo(ctx, fifos.Stdin, syscall.O_RDONLY, 0)
		assert.NoError(t, err)
	}

	if fifos.Stdout != "" {
		pipes.Stdout, err = fifo.OpenFifo(ctx, fifos.Stdout, syscall.O_WRONLY, 0)
		assert.NoError(t, err)
	}

	if fifos.Stderr != "" {
		pipes.Stderr, err = fifo.OpenFifo(ctx, fifos.Stderr, syscall.O_WRONLY, 0)
		assert.NoError(t, err)
	}

	return pipes
}

func initProducers(t *testing.T, producers producers, stdout, stderr string) {
	if producers.Stdout != nil {
		_, err := producers.Stdout.Write([]byte(stdout))
		assert.NoError(t, err)
		assert.Nil(t, producers.Stdout.Close())
	}

	if producers.Stderr != nil {
		_, err := producers.Stderr.Write([]byte(stderr))
		assert.NoError(t, err)
		assert.Nil(t, producers.Stderr.Close())
	}
}

func TestLogURIGenerator(t *testing.T) {
	baseTestLogURIGenerator(t, []LogURIGeneratorTestCase{
		{
			scheme:   "fifo",
			path:     "/full/path/pipe.fifo",
			expected: "fifo:///full/path/pipe.fifo",
		},
		{
			scheme: "file",
			path:   "/full/path/file.txt",
			args: map[string]string{
				"maxSize": "100MB",
			},
			expected: "file:///full/path/file.txt?maxSize=100MB",
		},
		{
			scheme: "binary",
			path:   "/full/path/bin",
			args: map[string]string{
				"id": "testing",
			},
			expected: "binary:///full/path/bin?id=testing",
		},
		{
			scheme: "unknown",
			path:   "nowhere",
			err:    "must be absolute",
		},
		{
			scheme: "binary",
			path:   "C:\\path\\to\\binary",
			// NOTE: Windows paths should not be parse-able outside of Windows:
			err: "must be absolute",
		},
	})
}
