// Copyright 2022 CoreOS, Inc.
//
// 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.

//go:build !windows
// +build !windows

package journal_test

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
	"testing"

	"github.com/coreos/go-systemd/v22/journal"
)

func TestJournalStreamParsing(t *testing.T) {
	if _, ok := os.LookupEnv("JOURNAL_STREAM"); ok {
		t.Fatal("unset JOURNAL_STREAM before running this test")
	}

	t.Run("Missing", func(t *testing.T) {
		ok, err := journal.StderrIsJournalStream()
		if err != nil {
			t.Fatal(err)
		}
		if ok {
			t.Error("stderr shouldn't be connected to journal stream")
		}
	})
	t.Run("Present", func(t *testing.T) {
		f, stat := getUnixStreamSocket(t)
		defer f.Close()
		os.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", stat.Dev, stat.Ino))
		defer os.Unsetenv("JOURNAL_STREAM")
		replaceStderr(int(f.Fd()), func() {
			ok, err := journal.StderrIsJournalStream()
			if err != nil {
				t.Fatal(err)
			}
			if !ok {
				t.Error("stderr should've been connected to journal stream")
			}
		})
	})
	t.Run("NotMatching", func(t *testing.T) {
		f, stat := getUnixStreamSocket(t)
		defer f.Close()
		os.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d:%d", stat.Dev+1, stat.Ino))
		defer os.Unsetenv("JOURNAL_STREAM")
		replaceStderr(int(f.Fd()), func() {
			ok, err := journal.StderrIsJournalStream()
			if err != nil {
				t.Fatal(err)
			}
			if ok {
				t.Error("stderr shouldn't be connected to journal stream")
			}
		})
	})
	t.Run("Malformed", func(t *testing.T) {
		f, stat := getUnixStreamSocket(t)
		defer f.Close()
		os.Setenv("JOURNAL_STREAM", fmt.Sprintf("%d-%d", stat.Dev, stat.Ino))
		defer os.Unsetenv("JOURNAL_STREAM")
		replaceStderr(int(f.Fd()), func() {
			_, err := journal.StderrIsJournalStream()
			if err == nil {
				t.Fatal("JOURNAL_STREAM is malformed, but no error returned")
			}
		})
	})
}

func TestStderrIsJournalStream(t *testing.T) {
	const (
		message = "TEST_MESSAGE"
	)

	userOrSystem := "--user"
	if os.Getuid() == 0 {
		userOrSystem = "--system"
	}

	if _, ok := os.LookupEnv("JOURNAL_STREAM"); !ok {
		// Re-execute this test under systemd (see the else branch),
		// and observe its exit code.
		args := []string{
			"systemd-run",
			userOrSystem,
			"--wait",
			"--quiet",
			"--",
			os.Args[0],
			"-test.run=TestStderrIsJournalStream",
			"-test.count=1", // inhibit caching
		}

		cmd := exec.Command(args[0], args[1:]...)
		cmd.Stderr = os.Stderr
		if err := cmd.Run(); err != nil {
			t.Fatal(err)
		}
	} else {
		ok, err := journal.StderrIsJournalStream()
		if err != nil {
			t.Fatal(err)
		}
		if !ok {
			t.Fatal("StderrIsJournalStream should've returned true")
		}

		err = journal.Send(message, journal.PriInfo, nil)
		if err != nil {
			t.Fatal(err)
		}
	}
}

func ExampleStderrIsJournalStream() {
	// NOTE: this is just an example. Production code
	// will likely use this to setup a logging library
	// to write messages to either journal or stderr.
	ok, err := journal.StderrIsJournalStream()
	if err != nil {
		panic(err)
	}

	if ok {
		// use journal native protocol
		journal.Send("this is a message logged through the native protocol", journal.PriInfo, nil)
	} else {
		// use stderr
		fmt.Fprintln(os.Stderr, "this is a message logged through stderr")
	}
}

func replaceStderr(fd int, cb func()) {
	savedStderr, err := syscall.Dup(syscall.Stderr)
	if err != nil {
		panic(err)
	}
	defer syscall.Close(savedStderr)
	err = syscall.Dup2(fd, syscall.Stderr)
	if err != nil {
		panic(err)
	}
	defer func() {
		err := syscall.Dup2(savedStderr, syscall.Stderr)
		if err != nil {
			panic(err)
		}
	}()
	cb()
}

// getUnixStreamSocket returns a unix stream socket obtained with
// socketpair(2), and its fstat result. Only one end of the socket pair
// is returned, and the other end is closed immediately: we don't need
// it for our purposes.
func getUnixStreamSocket(t *testing.T) (*os.File, *syscall.Stat_t) {
	fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
	if err != nil {
		t.Fatal(os.NewSyscallError("socketpair", err))
	}
	// we don't need the remote end for our tests
	syscall.Close(fds[1])

	file := os.NewFile(uintptr(fds[0]), "unix-stream")
	stat, err := file.Stat()
	if err != nil {
		t.Fatal(err)
	}
	return file, stat.Sys().(*syscall.Stat_t)
}
