// SPDX-License-Identifier: Apache-2.0
/*
 * umoci: Umoci Modifies Open Containers' Images
 * Copyright (C) 2016-2025 SUSE LLC
 *
 * 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 umoci

import (
	"bytes"
	"testing"
	"time"

	"github.com/opencontainers/go-digest"
	imeta "github.com/opencontainers/image-spec/specs-go"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPprint(t *testing.T) {
	for _, test := range []struct {
		name     string
		key      string
		values   []string
		expected string
	}{
		{"NoValue", "KeyA", nil, "KeyA:\n"},
		{"OneValue", "KeyB", []string{"foobar"}, "KeyB: foobar\n"},
		{"MultipleValues", "Multiple Values", []string{"a", "b", "c", "d", "e"}, "Multiple Values: a, b, c, d, e\n"},
		{"CommaValue", "Has a comma", []string{"a, b, c, d, e"}, `Has a comma: "a, b, c, d, e"` + "\n"},
		{"CommaValues", "Has commas", []string{"a,b", "c,d,e"}, `Has commas: "a,b", "c,d,e"` + "\n"},
		{"QuotedValue", "Funky value", []string{"foo bar\tba\\z"}, `Funky value: "foo bar\tba\\z"` + "\n"},
		{"QuotedValues", "Funky Values", []string{"a b", "c\nd", "e\t\bf", "foobar-baz"}, `Funky Values: "a b", "c\nd", "e\t\bf", foobar-baz` + "\n"},
		{"WhitespaceValue", "whitespace value", []string{"a b"}, `whitespace value: "a b"` + "\n"},
		{"WhitespaceValues", "multiple whitespace values", []string{"a b", "foo\u00a0bar", "abc\u202fdef"}, `multiple whitespace values: "a b", "foo\u00a0bar", "abc\u202fdef"` + "\n"},
	} {
		for _, prefix := range []struct {
			name, prefix string
		}{
			{"-NoPrefix", ""},
			{"-PrefixTab", "\t"},
			{"-PrefixTabs", "\t\t"},
			{"-PrefixOther", "...\t"},
		} {
			t.Run(test.name+prefix.name, func(t *testing.T) {
				var output bytes.Buffer
				err := pprint(&output, prefix.prefix, test.key, test.values...)
				require.NoErrorf(t, err, "pprint(%q, %q, %v...)", prefix.prefix, test.key, test.values)

				expected := prefix.prefix + test.expected
				assert.Equal(t, expected, output.String())
			})
		}
	}
}

func TestPprintSlice(t *testing.T) {
	for _, test := range []struct {
		name        string
		prefix, key string
		data        []string
		expected    string
	}{
		{"Nil", "\t", "Nil Values", nil, "\tNil Values: (empty)\n"},
		{"Empty", "\t\t", "Empty Values", []string{}, "\t\tEmpty Values: (empty)\n"},
		{"Basic", "\t\t\t", "Basic Values", []string{"a", "b", "cdef"}, "\t\t\t" + `Basic Values:
				a
				b
				cdef
`},
		{"Whitespace", "\t\t\t", "Whitespace Values", []string{" a b c   ", "foo  bar\n"}, "\t\t\t" + `Whitespace Values:
				" a b c   "
				"foo  bar\n"
`},
		{"Quoted", "\t\t\t", "Quoted Values", []string{"ab", "c\tdef", "foo-bar\n"}, "\t\t\t" + `Quoted Values:
				ab
				"c\tdef"
				"foo-bar\n"
`},
	} {
		t.Run(test.name, func(t *testing.T) {
			var output bytes.Buffer
			err := pprintSlice(&output, test.prefix, test.key, test.data)
			require.NoErrorf(t, err, "pprint(%q, %q, %v)", test.prefix, test.key, test.data)
			assert.Equal(t, test.expected, output.String())
		})
	}
}

func TestPprintMap(t *testing.T) {
	for _, test := range []struct {
		name        string
		prefix, key string
		data        map[string]string
		expected    string
	}{
		{"Nil", "\t", "Nil Map", nil, "\tNil Map: (empty)\n"},
		{"Empty", "\t\t", "Empty Map", map[string]string{}, "\t\tEmpty Map: (empty)\n"},
		{"Basic", "\t\t\t", "Basic Map", map[string]string{"a": "b", "c.e.f": "foobar123", "01": "2345"}, "\t\t\t" + `Basic Map:
				01: 2345
				a: b
				c.e.f: foobar123
`},
		{"Whitespace", "\t\t\t", "Whitespace Values", map[string]string{" a b c  ": "cdef", "foo": "foo  bar\n", "a b": "c d"}, "\t\t\t" + `Whitespace Values:
				" a b c  ": cdef
				"a b": "c d"
				foo: "foo  bar\n"
`},
		{"Quoted", "\t\t\t", "Quoted Map", map[string]string{"a\tb": "cdef", "ab": "c\tdef", "foo": "bar\n", "c\tef": "foo\u00a0bar123"}, "\t\t\t" + `Quoted Map:
				"a\tb": cdef
				ab: "c\tdef"
				"c\tef": "foo\u00a0bar123"
				foo: "bar\n"
`},
	} {
		t.Run(test.name, func(t *testing.T) {
			var output bytes.Buffer
			err := pprintMap(&output, test.prefix, test.key, test.data)
			require.NoErrorf(t, err, "pprint(%q, %q, %v)", test.prefix, test.key, test.data)
			assert.Equal(t, test.expected, output.String())
		})
	}
}

type set[T comparable] = map[T]struct{}

func mkset[T comparable](ks ...T) set[T] {
	set := make(set[T], len(ks))
	for _, k := range ks {
		set[k] = struct{}{}
	}
	return set
}

func TestPprintSet(t *testing.T) {
	for _, test := range []struct {
		name        string
		prefix, key string
		data        set[string]
		expected    string
	}{
		{"Nil", "\t", "Nil Set", nil, "\tNil Set: (empty)\n"},
		{"Empty", "\t\t", "Empty Set", set[string]{}, "\t\tEmpty Set: (empty)\n"},
		{"Basic", "\t\t\t", "Basic Set", mkset("a", "b", "c.e.f", "foobar123", "01", "2345"), "\t\t\tBasic Set: 01, 2345, a, b, c.e.f, foobar123\n"},
		{"Whitespace", "\t\t\t", "Whitespace Set", mkset(" a b c  ", "foo bar\n", "c\tef", "foo\u00a0bar123"), "\t\t\t" + `Whitespace Set: " a b c  ", "c\tef", "foo bar\n", "foo\u00a0bar123"` + "\n"},
		{"Quoted", "\t\t\t", "Quoted Set", mkset("a\tb", "cdef", "foo", "bar\n", "c\tef", "foo\u00a0bar123"), "\t\t\t" + `Quoted Set: "a\tb", "bar\n", "c\tef", cdef, foo, "foo\u00a0bar123"` + "\n"},
	} {
		t.Run(test.name, func(t *testing.T) {
			var output bytes.Buffer
			err := pprintSet(&output, test.prefix, test.key, test.data)
			require.NoErrorf(t, err, "pprint(%q, %q, %v)", test.prefix, test.key, test.data)
			assert.Equal(t, test.expected, output.String())
		})
	}
}

func TestPprintPlatform(t *testing.T) {
	for _, test := range []struct {
		name     string
		prefix   string
		platform ispec.Platform
		expected string
	}{
		{"Basic", "\t\t", ispec.Platform{
			OS:           "linux",
			Architecture: "amd64",
		}, "\t\t" + `Platform:
			OS: linux
			Architecture: amd64
`},
		{"ArchVariant", "\t\t", ispec.Platform{
			OS:           "freebsd",
			Architecture: "arm64",
			Variant:      "v7",
		}, "\t\t" + `Platform:
			OS: freebsd
			Architecture: arm64 (v7)
`},
		{"OSFeatures", "\t\t", ispec.Platform{
			OS:           "windows",
			OSVersion:    "10.0.14393.1066",
			OSFeatures:   []string{"win32k", "win-dummy"},
			Architecture: "amd64",
		}, "\t\t" + `Platform:
			OS: windows
			OS Version: 10.0.14393.1066
			OS Features: win32k, win-dummy
			Architecture: amd64
`},
		{"Whitespace", "\t\t", ispec.Platform{
			OS:           "fake os",
			OSVersion:    "a b c",
			OSFeatures:   []string{"a,b", "c d e f"},
			Architecture: "arm is cool",
			Variant:      "version 7",
		}, "\t\t" + `Platform:
			OS: "fake os"
			OS Version: "a b c"
			OS Features: "a,b", "c d e f"
			Architecture: "arm is cool" (version 7)
`},
		{"Quoted", "\t\t", ispec.Platform{
			OS:           "fake\tos\n",
			OSVersion:    "a\vbc",
			OSFeatures:   []string{"a\tb", "c\u00a0bar"},
			Architecture: "arm\nis\ncool",
			Variant:      "version\b7",
		}, "\t\t" + `Platform:
			OS: "fake\tos\n"
			OS Version: "a\vbc"
			OS Features: "a\tb", "c\u00a0bar"
			Architecture: "arm\nis\ncool" ("version\b7")
`},
	} {
		t.Run(test.name, func(t *testing.T) {
			var output bytes.Buffer
			err := pprintPlatform(&output, test.prefix, test.platform)
			require.NoErrorf(t, err, "pprint(%q, %#v)", test.prefix, test.platform)
			assert.Equal(t, test.expected, output.String())
		})
	}
}

func TestPprintDescriptor(t *testing.T) {
	for _, test := range []struct {
		name       string
		prefix     string
		descriptor ispec.Descriptor
		expected   string
	}{
		{"Empty", "\t\t", ispec.Descriptor{}, "\t\t" + `Descriptor:
			Media Type: ""
			Digest: ""
			Size: 0B
`},
		{"EmptyJSON", "\t\t", ispec.DescriptorEmptyJSON, "\t\t" + `Descriptor:
			Media Type: application/vnd.oci.empty.v1+json
			Digest: sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
			Size: 2B
			Data: (2 bytes)
				0000: 7b7d                                     {}
`},
		// Adapted from <https://github.com/opencontainers/image-spec/blob/v1.1.1/descriptor.md#examples>.
		{"SpecExample-1", "\t\t", ispec.Descriptor{
			MediaType: ispec.MediaTypeImageManifest,
			Digest:    "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
			Size:      7682,
		}, "\t\t" + `Descriptor:
			Media Type: application/vnd.oci.image.manifest.v1+json
			Digest: sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270
			Size: 7.682kB
`},
		{"SpecExample-2", "\t\t", ispec.Descriptor{
			MediaType: ispec.MediaTypeImageManifest,
			Digest:    "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
			Size:      7682,
			URLs: []string{
				"https://example.com/example-manifest",
			},
		}, "\t\t" + `Descriptor:
			Media Type: application/vnd.oci.image.manifest.v1+json
			Digest: sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270
			Size: 7.682kB
			URLs:
				https://example.com/example-manifest
`},
		{"SpecExample-3", "\t\t", ispec.Descriptor{
			MediaType:    "",
			ArtifactType: "application/vnd.example.sbom.v1",
			Digest:       "sha256:87923725d74f4bfb94c9e86d64170f7521aad8221a5de834851470ca142da630",
			Size:         123,
		}, "\t\t" + `Descriptor:
			Media Type: ""
			Artifact Type: application/vnd.example.sbom.v1
			Digest: sha256:87923725d74f4bfb94c9e86d64170f7521aad8221a5de834851470ca142da630
			Size: 123B
`},
		{"Basic", "\t\t", ispec.Descriptor{
			MediaType: "image/png",
			Digest:    "sha256:7d30ef97fe167eb78d5c52503096522ebba7ba95789ba735fd68da9d4838f84d",
			Size:      61235,
		}, "\t\t" + `Descriptor:
			Media Type: image/png
			Digest: sha256:7d30ef97fe167eb78d5c52503096522ebba7ba95789ba735fd68da9d4838f84d
			Size: 61.23kB
`},
		{"ArtifactType", "\t\t", ispec.Descriptor{
			MediaType:    ispec.MediaTypeEmptyJSON,
			ArtifactType: "image/png",
			Digest:       "sha256:7a30ef97fe167eb78d5c52503096522ebba7ba95789ba735fd68da9d4838f84d",
			Size:         82235,
		}, "\t\t" + `Descriptor:
			Media Type: application/vnd.oci.empty.v1+json
			Artifact Type: image/png
			Digest: sha256:7a30ef97fe167eb78d5c52503096522ebba7ba95789ba735fd68da9d4838f84d
			Size: 82.23kB
`},
		{"Annotations", "\t\t", ispec.Descriptor{
			MediaType: ispec.MediaTypeImageManifest,
			Digest:    "blake2:acbe05bb922855b8b2fea560e11ad2d135505a9f194a0fa99f1c57d3974990f6853ce22e97273d8dbf7424c09fe43e53ce4080dcc543482be46965bb24639cbe",
			Size:      59129319,
			Annotations: map[string]string{
				"com.example.foo": "abc123",
				"ci.umo.foobar":   "test",
				"com.cyphar.test": "https://www.cyphar.com/",
			},
		}, "\t\t" + `Descriptor:
			Media Type: application/vnd.oci.image.manifest.v1+json
			Digest: blake2:acbe05bb922855b8b2fea560e11ad2d135505a9f194a0fa99f1c57d3974990f6853ce22e97273d8dbf7424c09fe43e53ce4080dcc543482be46965bb24639cbe
			Size: 59.13MB
			Annotations:
				ci.umo.foobar: test
				com.cyphar.test: https://www.cyphar.com/
				com.example.foo: abc123
`},
		{"URLs", "\t\t", ispec.Descriptor{
			MediaType: ispec.MediaTypeImageLayerGzip,
			Digest:    "sha512:dd65b6a8d277c7b06003178fc7a125fa6ad021dff2beffd82df0b469f2afe735312e2932608d42d1bc8421141edb0882680e36d45f5d804783ef555a3566a9f8",
			Size:      926812731,
			URLs: []string{
				"https://www.example.com/example.txt",
				"https://www.cyphar.com/",
			},
		}, "\t\t" + `Descriptor:
			Media Type: application/vnd.oci.image.layer.v1.tar+gzip
			Digest: sha512:dd65b6a8d277c7b06003178fc7a125fa6ad021dff2beffd82df0b469f2afe735312e2932608d42d1bc8421141edb0882680e36d45f5d804783ef555a3566a9f8
			Size: 926.8MB
			URLs:
				https://www.example.com/example.txt
				https://www.cyphar.com/
`},
		{"Data-Short", "\t\t", ispec.Descriptor{
			MediaType: "application/text",
			Digest:    "sha256:aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f",
			Size:      7,
			Data:      []byte("foobar\n"),
		}, "\t\t" + `Descriptor:
			Media Type: application/text
			Digest: sha256:aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f
			Size: 7B
			Data: (7 bytes)
				0000: 666f 6f62 6172 0a                        foobar.
`},
		{"Data-ShortMultiline", "\t\t", ispec.Descriptor{
			MediaType: "application/text",
			Digest:    "sha256:b47cc0f104b62d4c7c30bcd68fd8e67613e287dc4ad8c310ef10cbadea9c4380",
			Size:      45,
			Data:      []byte("The quick brown fox jumps over the lazy dog.\n"),
		}, "\t\t" + `Descriptor:
			Media Type: application/text
			Digest: sha256:b47cc0f104b62d4c7c30bcd68fd8e67613e287dc4ad8c310ef10cbadea9c4380
			Size: 45B
			Data: (45 bytes)
				0000: 5468 6520 7175 6963 6b20 6272 6f77 6e20  The quick brown ` + `
				0010: 666f 7820 6a75 6d70 7320 6f76 6572 2074  fox jumps over t
				0020: 6865 206c 617a 7920 646f 672e 0a         he lazy dog..
`},
		{"Data-Long", "\t\t", ispec.Descriptor{
			MediaType: "legal/x-license; Apache-2.0",
			Digest:    "sha256:af38b8d8986cfc6f3132e289f5e6abf0facdf72f1f67c0f87fa8db3e2e8a9c55",
			Size:      524,
			Data: []byte(`
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.
`),
		}, "\t\t" + `Descriptor:
			Media Type: "legal/x-license; Apache-2.0"
			Digest: sha256:af38b8d8986cfc6f3132e289f5e6abf0facdf72f1f67c0f87fa8db3e2e8a9c55
			Size: 524B
			Data: (524 bytes)
				0000: 0a20 2020 2020 2020 2020 2020 2020 2020  .               ` + `
				0010: 2020 2020 2020 2020 2020 2020 2020 2020                  ` + `
				0020: 2020 4170 6163 6865 204c 6963 656e 7365    Apache License` + `
				0030: 0a20 2020 2020 2020 2020 2020 2020 2020  .               ` + `
				0040: 2020 2020 2020 2020 2020 2020 5665 7273              Vers` + `
				0050: 696f 6e20 322e 302c 204a 616e 7561 7279  ion 2.0, January` + `
				0060: 2032 3030 340a 2020 2020 2020 2020 2020   2004.          ` + `
				0070: 2020 2020 2020 2020 2020 2020 2020 6874                ht` + `
				0080: 7470 3a2f 2f77 7777 2e61 7061 6368 652e  tp://www.apache.` + `
				0090: 6f72 672f 6c69 6365 6e73 6573 2f0a 0a20  org/licenses/.. ` + `
				00a0: 2020 5445 524d 5320 414e 4420 434f 4e44    TERMS AND COND` + `
				00b0: 4954 494f 4e53 2046 4f52 2055 5345 2c20  ITIONS FOR USE, ` + `
				00c0: 5245 5052 4f44 5543 5449 4f4e 2c20 414e  REPRODUCTION, AN` + `
				00d0: 4420 4449 5354 5249 4255 5449 4f4e 0a0a  D DISTRIBUTION..` + `
				00e0: 2020 2031 2e20 4465 6669 6e69 7469 6f6e     1. Definition` + `
				00f0: 732e 0a0a 2020 2020 2020 224c 6963 656e  s...      "Licen` + `
				....  (extra 268 bytes omitted)
`},
		{"Whitespace", "\t\t", ispec.Descriptor{
			MediaType: "application/x-dummy; foo",
			Digest:    "dummy: a b c d",
			Size:      123,
			URLs: []string{
				"https:// www. example.com/example.txt",
				"https:// www. cyphar.com/",
			},
			Annotations: map[string]string{
				"com. example.foo": "abc123",
				"ci.umo.foobar":    "test foo",
				"com cyphar test":  "https   www cyphar com ",
			},
		}, "\t\t" + `Descriptor:
			Media Type: "application/x-dummy; foo"
			Digest: "dummy: a b c d"
			Size: 123B
			URLs:
				"https:// www. example.com/example.txt"
				"https:// www. cyphar.com/"
			Annotations:
				ci.umo.foobar: "test foo"
				"com cyphar test": "https   www cyphar com "
				"com. example.foo": abc123
`},
		{"Quoted", "\t\t", ispec.Descriptor{
			MediaType: "application/x-dummy;\nfoo",
			Digest:    "dummy:a\bb\bc\td",
			Size:      123,
			URLs: []string{
				"\t\n",
			},
			Annotations: map[string]string{
				"com.\texample.foo": "abc123",
				"ci.umo.foobar":     "test\nfoo",
				"com cyphar test":   "https \t www cyphar com ",
			},
		}, "\t\t" + `Descriptor:
			Media Type: "application/x-dummy;\nfoo"
			Digest: "dummy:a\bb\bc\td"
			Size: 123B
			URLs:
				"\t\n"
			Annotations:
				ci.umo.foobar: "test\nfoo"
				"com cyphar test": "https \t www cyphar com "
				"com.\texample.foo": abc123
`},
	} {
		t.Run(test.name, func(t *testing.T) {
			var output bytes.Buffer
			err := pprintDescriptor(&output, test.prefix, test.descriptor)
			require.NoErrorf(t, err, "pprint(%q, %#v)", test.prefix, test.descriptor)
			assert.Equal(t, test.expected, output.String())
		})
	}
}

func TestPprintImageConfig(t *testing.T) {
	for _, test := range []struct {
		name     string
		prefix   string
		config   ispec.ImageConfig
		expected string
	}{
		{"Empty", "\t\t", ispec.ImageConfig{}, "\t\t" + `Image Config:
			User: ""
			Command: (empty)
`},
		{"User", "\t\t", ispec.ImageConfig{
			User: "foo:bar",
		}, "\t\t" + `Image Config:
			User: foo:bar
			Command: (empty)
`},
		{"Command", "\t\t", ispec.ImageConfig{
			User: "0",
			Cmd:  []string{"/bin/bash", "-c", "while true; sleep 1s; done;"},
		}, "\t\t" + `Image Config:
			User: 0
			Command:
				/bin/bash
				-c
				"while true; sleep 1s; done;"
`},
		{"Entrypoint", "\t\t", ispec.ImageConfig{
			User:       "0",
			Entrypoint: []string{"/bin/bash", "-c"},
		}, "\t\t" + `Image Config:
			User: 0
			Entrypoint:
				/bin/bash
				-c
			Command: (empty)
`},
		{"Entrypoint+Command", "\t\t", ispec.ImageConfig{
			User:       "0",
			Entrypoint: []string{"/bin/bash", "-c"},
			Cmd:        []string{"while true; sleep 1s; done;"},
		}, "\t\t" + `Image Config:
			User: 0
			Entrypoint:
				/bin/bash
				-c
			Command:
				"while true; sleep 1s; done;"
`},
		{"Command+Entrypoint-Escaped", "\t\t", ispec.ImageConfig{
			User:        "0",
			ArgsEscaped: true,
			Entrypoint:  []string{"/bin/bash -c "},
			Cmd:         []string{"while true; sleep 1s; done;"},
		}, "\t\t" + `Image Config:
			User: 0
			Entrypoint (escaped):
				"/bin/bash -c "
			Command (escaped):
				"while true; sleep 1s; done;"
`},
		{"Environment", "\t\t", ispec.ImageConfig{
			Env: []string{
				"PATH=/bin:/usr/bin",
				"HOME=/home/cyphar",
				"FOOBAR=The quick brown fox jumps over the lazy dog.",
				"EQUAL==",
			},
		}, "\t\t" + `Image Config:
			User: ""
			Command: (empty)
			Environment:
				PATH=/bin:/usr/bin
				HOME=/home/cyphar
				"FOOBAR=The quick brown fox jumps over the lazy dog."
				EQUAL==
`},
		{"StopSignal", "\t\t", ispec.ImageConfig{
			StopSignal: "SIGKILL",
		}, "\t\t" + `Image Config:
			User: ""
			Command: (empty)
			Stop Signal: SIGKILL
`},
		{"ExposedPorts", "\t\t", ispec.ImageConfig{
			ExposedPorts: mkset("80/tcp", "50000/udp"),
		}, "\t\t" + `Image Config:
			User: ""
			Command: (empty)
			Exposed Ports: 50000/udp, 80/tcp
`},
		{"Volumes", "\t\t", ispec.ImageConfig{
			Volumes: mkset("/foo/bar", "/baz", "/beep boop"),
		}, "\t\t" + `Image Config:
			User: ""
			Command: (empty)
			Volumes: /baz, "/beep boop", /foo/bar
`},
		{"Labels", "\t\t", ispec.ImageConfig{
			Labels: map[string]string{
				"foo bar": "baz",
			},
		}, "\t\t" + `Image Config:
			User: ""
			Command: (empty)
			Labels:
				"foo bar": baz
`},
		{"Basic", "\t\t", ispec.ImageConfig{
			Cmd:        []string{"bash"},
			WorkingDir: "/go",
			Env: []string{
				"PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
				"GOLANG_VERSION=1.25.1",
				"GOTOOLCHAIN=local",
				"GOPATH=/go",
			},
		}, "\t\t" + `Image Config:
			User: ""
			Command:
				bash
			Working Directory: /go
			Environment:
				PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
				GOLANG_VERSION=1.25.1
				GOTOOLCHAIN=local
				GOPATH=/go
`},
	} {
		t.Run(test.name, func(t *testing.T) {
			var output bytes.Buffer
			err := pprintImageConfig(&output, test.prefix, test.config)
			require.NoErrorf(t, err, "pprint(%q, %#v)", test.prefix, test.config)
			assert.Equal(t, test.expected, output.String())
		})
	}
}

func mktime(sec, nsec int64) *time.Time {
	time := time.Unix(sec, nsec).UTC()
	return &time
}

func TestPprintImage(t *testing.T) {
	for _, test := range []struct {
		name     string
		prefix   string
		image    ispec.Image
		expected string
	}{
		{"Empty", "\t\t", ispec.Image{}, "\t\t" + `Author: ""
		Platform:
			OS: ""
			Architecture: ""
		Image Config:
			User: ""
			Command: (empty)
`},
		// Adapted from <https://github.com/opencontainers/image-spec/blob/v1.1.1/config.md#example>.
		{"SpecExample", "\t\t", ispec.Image{
			Created: mktime(1446330176, 15925234),
			Author:  "Alyssa P. Hacker <alyspdev@example.com>",
			Platform: ispec.Platform{
				OS:           "linux",
				Architecture: "amd64",
			},
			Config: ispec.ImageConfig{
				User:       "alice",
				Entrypoint: []string{"/bin/my-app-binary"},
				Cmd:        []string{"--foreground", "--config", "/etc/my-app.d/default.cfg"},
				WorkingDir: "/home/alice",
				Env: []string{
					"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
					"FOO=oci_is_a",
					"BAR=well_written_spec",
				},
				ExposedPorts: mkset("8080/tcp"),
				Volumes:      mkset("/var/job-result-data", "/var/log/my-app-logs"),
				Labels: map[string]string{
					"com.example.project.git.url":    "https://example.com/project.git",
					"com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b",
				},
			},
			// Not printed by pprintImage, as this is handled by the history
			// pretty-printer.
			RootFS: ispec.RootFS{
				Type: "layers",
				DiffIDs: []digest.Digest{
					"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
					"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
				},
			},
			History: []ispec.History{
				{
					Created:   mktime(1446330174, 690851953),
					CreatedBy: "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /",
				},
				{
					Created:    mktime(1446330175, 613815829),
					CreatedBy:  "/bin/sh -c #(nop) CMD [\"sh\"]",
					EmptyLayer: true,
				},
				{
					Created:   mktime(1446330176, 329850019),
					CreatedBy: "/bin/sh -c apk add curl",
				},
			},
		}, "\t\t" + `Created: 2015-10-31T22:22:56.015925234Z
		Author: "Alyssa P. Hacker <alyspdev@example.com>"
		Platform:
			OS: linux
			Architecture: amd64
		Image Config:
			User: alice
			Entrypoint:
				/bin/my-app-binary
			Command:
				--foreground
				--config
				/etc/my-app.d/default.cfg
			Working Directory: /home/alice
			Environment:
				PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
				FOO=oci_is_a
				BAR=well_written_spec
			Exposed Ports: 8080/tcp
			Volumes: /var/job-result-data, /var/log/my-app-logs
			Labels:
				com.example.project.git.commit: 45a939b2999782a3f005621a8d0f29aa387e1d6b
				com.example.project.git.url: https://example.com/project.git
`},
		{"Basic", "\t\t", ispec.Image{
			Created: mktime(1757077500, 0),
			Author:  "Aleksa Sarai <cyphar@cyphar.com>",
			Platform: ispec.Platform{
				OS:           "linux",
				Architecture: "amd64",
			},
			Config: ispec.ImageConfig{
				User:         "root",
				Entrypoint:   []string{"/bin/bash", "-c"},
				Cmd:          []string{"shutdown -h"},
				WorkingDir:   "/tmp",
				Env:          []string{"PATH=/bin:/usr/bin", "HOME=/"},
				StopSignal:   "SIGSTOP",
				ExposedPorts: mkset("12345/tcp"),
				Volumes:      mkset("/tmp", "/opt/foo/bar"),
				Labels: map[string]string{
					"org.opencontainers.image.url": "https://www.cyphar.com/",
					"org.label-schema.descripton":  "foo bar baz",
				},
			},
		}, "\t\t" + `Created: 2025-09-05T13:05:00Z
		Author: "Aleksa Sarai <cyphar@cyphar.com>"
		Platform:
			OS: linux
			Architecture: amd64
		Image Config:
			User: root
			Entrypoint:
				/bin/bash
				-c
			Command:
				"shutdown -h"
			Working Directory: /tmp
			Environment:
				PATH=/bin:/usr/bin
				HOME=/
			Stop Signal: SIGSTOP
			Exposed Ports: 12345/tcp
			Volumes: /opt/foo/bar, /tmp
			Labels:
				org.label-schema.descripton: "foo bar baz"
				org.opencontainers.image.url: https://www.cyphar.com/
`},
	} {
		t.Run(test.name, func(t *testing.T) {
			var output bytes.Buffer
			err := pprintImage(&output, test.prefix, test.image)
			require.NoErrorf(t, err, "pprint(%q, %#v)", test.prefix, test.image)
			assert.Equal(t, test.expected, output.String())
		})
	}
}

func TestPprintManifest(t *testing.T) {
	for _, test := range []struct {
		name     string
		prefix   string
		manifest ispec.Manifest
		expected string
	}{
		{"Empty", "\t\t", ispec.Manifest{}, "\t\t" + `Schema Version: 0
		Media Type: ""
		Config:
			Descriptor:
				Media Type: ""
				Digest: ""
				Size: 0B
`},
		// Adapted from <https://github.com/opencontainers/image-spec/blob/v1.1.1/manifest.md#example-image-manifest>.
		{"SpecExample", "\t\t\t", ispec.Manifest{
			Versioned: imeta.Versioned{SchemaVersion: 2},
			MediaType: ispec.MediaTypeImageManifest,
			Config: ispec.Descriptor{
				MediaType: ispec.MediaTypeImageConfig,
				Digest:    "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7",
				Size:      7023,
			},
			Layers: []ispec.Descriptor{
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0",
					Size:      32654,
				},
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b",
					Size:      16724,
				},
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736",
					Size:      73109,
				},
			},
			Subject: &ispec.Descriptor{
				MediaType: ispec.MediaTypeImageManifest,
				Digest:    "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
				Size:      7682,
			},
			Annotations: map[string]string{
				"com.example.key1": "value1",
				"com.example.key2": "value2",
			},
		}, "\t\t\t" + `Schema Version: 2
			Media Type: application/vnd.oci.image.manifest.v1+json
			Config:
				Descriptor:
					Media Type: application/vnd.oci.image.config.v1+json
					Digest: sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7
					Size: 7.023kB
			Layers:
				Descriptor:
					Media Type: application/vnd.oci.image.layer.v1.tar+gzip
					Digest: sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0
					Size: 32.65kB
				Descriptor:
					Media Type: application/vnd.oci.image.layer.v1.tar+gzip
					Digest: sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b
					Size: 16.72kB
				Descriptor:
					Media Type: application/vnd.oci.image.layer.v1.tar+gzip
					Digest: sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736
					Size: 73.11kB
			Subject:
				Descriptor:
					Media Type: application/vnd.oci.image.manifest.v1+json
					Digest: sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270
					Size: 7.682kB
			Annotations:
				com.example.key1: value1
				com.example.key2: value2
`},
		// Adapted from <https://github.com/opencontainers/image-spec/blob/v1.1.1/manifest.md#guidelines-for-artifact-usage>.
		{"ArtifactExample-1", "\t\t\t", ispec.Manifest{
			Versioned:    imeta.Versioned{SchemaVersion: 2},
			MediaType:    ispec.MediaTypeImageManifest,
			ArtifactType: "application/vnd.example+type",
			Config:       ispec.DescriptorEmptyJSON,
			Layers:       []ispec.Descriptor{ispec.DescriptorEmptyJSON},
			Annotations: map[string]string{
				"oci.opencontainers.image.created": "2023-01-02T03:04:05Z",
				"com.example.data":                 "payload",
			},
		}, "\t\t\t" + `Schema Version: 2
			Media Type: application/vnd.oci.image.manifest.v1+json
			Artifact Type: application/vnd.example+type
			Config:
				Descriptor:
					Media Type: application/vnd.oci.empty.v1+json
					Digest: sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
					Size: 2B
					Data: (2 bytes)
						0000: 7b7d                                     {}
			Layers:
				Descriptor:
					Media Type: application/vnd.oci.empty.v1+json
					Digest: sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
					Size: 2B
					Data: (2 bytes)
						0000: 7b7d                                     {}
			Annotations:
				com.example.data: payload
				oci.opencontainers.image.created: 2023-01-02T03:04:05Z
`},
		{"ArtifactExample-2", "\t\t\t", ispec.Manifest{
			Versioned:    imeta.Versioned{SchemaVersion: 2},
			MediaType:    ispec.MediaTypeImageManifest,
			ArtifactType: "application/vnd.example+type",
			Config:       ispec.DescriptorEmptyJSON,
			Layers: []ispec.Descriptor{
				{
					MediaType: "application/vnd.example+type",
					Digest:    "sha256:e258d248fda94c63753607f7c4494ee0fcbe92f1a76bfdac795c9d84101eb317",
					Size:      1234,
				},
			},
		}, "\t\t\t" + `Schema Version: 2
			Media Type: application/vnd.oci.image.manifest.v1+json
			Artifact Type: application/vnd.example+type
			Config:
				Descriptor:
					Media Type: application/vnd.oci.empty.v1+json
					Digest: sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a
					Size: 2B
					Data: (2 bytes)
						0000: 7b7d                                     {}
			Layers:
				Descriptor:
					Media Type: application/vnd.example+type
					Digest: sha256:e258d248fda94c63753607f7c4494ee0fcbe92f1a76bfdac795c9d84101eb317
					Size: 1.234kB
`},
		{"ArtifactExample-3", "\t\t\t", ispec.Manifest{
			Versioned:    imeta.Versioned{SchemaVersion: 2},
			MediaType:    ispec.MediaTypeImageManifest,
			ArtifactType: "application/vnd.example+type",
			Config: ispec.Descriptor{
				MediaType: "application/vnd.example.config.v1+json",
				Digest:    "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
				Size:      123,
			},
			Layers: []ispec.Descriptor{
				{
					MediaType: "application/vnd.example+type",
					Digest:    "sha256:e258d248fda94c63753607f7c4494ee0fcbe92f1a76bfdac795c9d84101eb317",
					Size:      1234,
				},
			},
		}, "\t\t\t" + `Schema Version: 2
			Media Type: application/vnd.oci.image.manifest.v1+json
			Artifact Type: application/vnd.example+type
			Config:
				Descriptor:
					Media Type: application/vnd.example.config.v1+json
					Digest: sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
					Size: 123B
			Layers:
				Descriptor:
					Media Type: application/vnd.example+type
					Digest: sha256:e258d248fda94c63753607f7c4494ee0fcbe92f1a76bfdac795c9d84101eb317
					Size: 1.234kB
`},
		{"Basic", "\t\t", ispec.Manifest{
			Versioned: imeta.Versioned{SchemaVersion: 2},
			MediaType: ispec.MediaTypeImageManifest,
			Config: ispec.Descriptor{
				MediaType: ispec.MediaTypeImageConfig,
				Digest:    "sha256:6d79abd4c96299aa91f5a4a46551042407568a3858b00ab460f4ba430984f62c",
				Size:      2297,
			},
			Layers: []ispec.Descriptor{
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:15b1d8a5ff03aeb0f14c8d39a60a73ef22f656550bfa1bb90d1850f25a0ac0fa",
					Size:      49279531,
				},
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:22718812f617084a0c5e539e02723b75bf79ea2a589430f820efcbb07f45b91b",
					Size:      25613635,
				},
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:401a98f7495bee3e8e6943da9f52f0ab1043c43eb1d107a3817fc2a4b916be97",
					Size:      67776756,
				},
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:1c315634bf9079ab45072808dc1101241352a3762e0bb1ec75369a0f65672ab0",
					Size:      102071864,
				},
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:330457f0054be4c07fbaeac90483ac4f534113ccd944fe16beb9e079a3ab3a36",
					Size:      60045609,
				},
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:771ee02be966963d69210ed8243baa8f322661858bae69d1c2d5d13fe4dc92ba",
					Size:      126,
				},
				{
					MediaType: ispec.MediaTypeImageLayerGzip,
					Digest:    "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
					Size:      32,
				},
			},
			Annotations: map[string]string{
				"com.docker.official-images.bashbrew.arch": "amd64",
				"org.opencontainers.image.base.digest":     "sha256:6f9fd607b2e260d41c51207fb74010ba06931047760494f5f6d59854e7d55db4",
				"org.opencontainers.image.base.name":       "buildpack-deps:trixie-scm",
				"org.opencontainers.image.created":         "2025-09-03T18:13:04Z",
				"org.opencontainers.image.revision":        "be5c27da377afdeebf0c5747560bedd7f96ccb1c",
				"org.opencontainers.image.source":          "https://github.com/docker-library/golang.git#be5c27da377afdeebf0c5747560bedd7f96ccb1c:1.25/trixie",
				"org.opencontainers.image.url":             "https://hub.docker.com/_/golang",
				"org.opencontainers.image.version":         "1.25.1-trixie",
			},
		}, "\t\t" + `Schema Version: 2
		Media Type: application/vnd.oci.image.manifest.v1+json
		Config:
			Descriptor:
				Media Type: application/vnd.oci.image.config.v1+json
				Digest: sha256:6d79abd4c96299aa91f5a4a46551042407568a3858b00ab460f4ba430984f62c
				Size: 2.297kB
		Layers:
			Descriptor:
				Media Type: application/vnd.oci.image.layer.v1.tar+gzip
				Digest: sha256:15b1d8a5ff03aeb0f14c8d39a60a73ef22f656550bfa1bb90d1850f25a0ac0fa
				Size: 49.28MB
			Descriptor:
				Media Type: application/vnd.oci.image.layer.v1.tar+gzip
				Digest: sha256:22718812f617084a0c5e539e02723b75bf79ea2a589430f820efcbb07f45b91b
				Size: 25.61MB
			Descriptor:
				Media Type: application/vnd.oci.image.layer.v1.tar+gzip
				Digest: sha256:401a98f7495bee3e8e6943da9f52f0ab1043c43eb1d107a3817fc2a4b916be97
				Size: 67.78MB
			Descriptor:
				Media Type: application/vnd.oci.image.layer.v1.tar+gzip
				Digest: sha256:1c315634bf9079ab45072808dc1101241352a3762e0bb1ec75369a0f65672ab0
				Size: 102.1MB
			Descriptor:
				Media Type: application/vnd.oci.image.layer.v1.tar+gzip
				Digest: sha256:330457f0054be4c07fbaeac90483ac4f534113ccd944fe16beb9e079a3ab3a36
				Size: 60.05MB
			Descriptor:
				Media Type: application/vnd.oci.image.layer.v1.tar+gzip
				Digest: sha256:771ee02be966963d69210ed8243baa8f322661858bae69d1c2d5d13fe4dc92ba
				Size: 126B
			Descriptor:
				Media Type: application/vnd.oci.image.layer.v1.tar+gzip
				Digest: sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1
				Size: 32B
		Annotations:
			com.docker.official-images.bashbrew.arch: amd64
			org.opencontainers.image.base.digest: sha256:6f9fd607b2e260d41c51207fb74010ba06931047760494f5f6d59854e7d55db4
			org.opencontainers.image.base.name: buildpack-deps:trixie-scm
			org.opencontainers.image.created: 2025-09-03T18:13:04Z
			org.opencontainers.image.revision: be5c27da377afdeebf0c5747560bedd7f96ccb1c
			org.opencontainers.image.source: https://github.com/docker-library/golang.git#be5c27da377afdeebf0c5747560bedd7f96ccb1c:1.25/trixie
			org.opencontainers.image.url: https://hub.docker.com/_/golang
			org.opencontainers.image.version: 1.25.1-trixie
`},
	} {
		t.Run(test.name, func(t *testing.T) {
			var output bytes.Buffer
			err := pprintManifest(&output, test.prefix, test.manifest)
			require.NoErrorf(t, err, "pprint(%q, %#v)", test.prefix, test.manifest)
			assert.Equal(t, test.expected, output.String())
		})
	}
}
