// Copyright © 2020 Intel Corporation
//
// SPDX-License-Identifier: GPL-3.0-only

package model

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/clearlinux/clr-installer/args"
	"github.com/clearlinux/clr-installer/storage"
	"github.com/clearlinux/clr-installer/user"
	"github.com/clearlinux/clr-installer/utils"
)

func init() {
	utils.SetLocale("en_US.UTF-8")
}

var (
	testsDir string
)

func init() {
	testsDir = os.Getenv("TESTS_DIR")
	testAlias = append(testAlias, "/dev/sda", "/dev/sdb")
}

func TestLoadFile(t *testing.T) {
	tests := []struct {
		file  string
		valid bool
	}{
		{"baseline.yaml", true},
		{"image-generation.yaml", true},
		{"basic-invalid-descriptor.yaml", false},
		{"basic-valid-descriptor.yaml", true},
		{"invalid-no-keyboard.yaml", false},
		{"invalid-no-language.yaml", false},
		{"malformed-descriptor.yaml", false},
		{"no-bootable-descriptor.yaml", false},
		{"no-root-partition-descriptor.yaml", false},
		{"no-telemetry.yaml", false},
		{"invalid-no-kernel.yaml", false},
		{"block-device-image.yaml", true},
		{"block-devices-alias.yaml", true},
		{"mixed-block-device.yaml", true},
		{"real-example.yaml", true},
		{"user-sshkeys.yaml", true},
		{"valid-minimal.yaml", true},
		{"valid-network.yaml", true},
		{"valid-with-pre-post-hooks.yaml", true},
		{"valid-with-version.yaml", true},
		{"iso-bad.yaml", false},
		{"iso-good.yaml", true},
		{"iso-desktop.yaml", true},
		{"azure-config.json", true},
		{"azure-docker-config.json", true},
		{"azure-machine-learning-config.json", true},
		{"ciao-networking-config.json", true},
		{"cloud-config.json", true},
		{"cloud-docker-config.json", true},
		{"gce-config.json", true},
		{"hyperv-config.json", true},
		{"hyperv-mini-config.json", true},
		{"hyperv-test-config.json", true},
		{"kvm-config.json", true},
		{"legacy-kvm-config.json", true},
		{"live-config.json", true},
		{"live-docker-config.json", true},
		{"provision-config.json", true},
		{"vmware-config.json", true},
		{"mbr.json", true},
		{"min-good.json", false},
		{"release-image-config.json", true},
		{"full-good.json", true},
		{"installer-config.json", true},
		{"installer-config-vm.json", true},
		{"ister.json", true},
		{"kernels.json", true},
		{"valid-ister-full-virtual.json", true},
		{"valid-ister-full-physical.json", true},
		{"invalid-ister-basic-descriptor.json", false},
		{"invalid-ister-no-kernel.json", false},
		{"invalid-ister-malformed-descriptor.json", false},
		{"invalid-ister-dt.json", false},
		{"invalid-ister-missing-pl.json", false},
		{"invalid-ister-duplicate-pl.json", false},
		{"invalid-ister-disk-ft.json", false},
		{"invalid-ister-partition-ft.json", false},
		{"invalid-ister-disk-pmp.json", false},
		{"invalid-ister-partition-pmp.json", false},
	}

	for _, curr := range tests {
		path := filepath.Join(testsDir, curr.file)
		t.Logf("Loading test file %v", curr)
		if filepath.Ext(curr.file) == ".json" {
			md, err := JSONtoYAMLConfig(path)
			if err == nil {
				path, err = md.WriteYAMLConfig(path)
				defer func() {
					_ = os.Remove(path)
				}()
			}

			if curr.valid && err != nil {
				t.Fatalf("%s is a valid test and shouldn't return an error: %v", curr.file, err)
			}
		}

		model, err := LoadFile(path, args.Args{})

		if curr.valid && err != nil {
			t.Fatalf("%s is a valid tests and shouldn't return an error: %v", curr.file, err)
		}

		if model != nil {
			model.MediaOpts.SkipValidationSize = true
			model.MediaOpts.SkipValidationAll = true
		}
		err = model.Validate()
		if curr.valid && err != nil {
			t.Fatalf("%s is a valid tests and shouldn't return an error: %v", curr.file, err)
		}
	}
}

func TestIsTestAlias(t *testing.T) {
	testAlias = []string{}

	if isTestAlias("/dev/sda") {
		t.Fatalf("Should have returned false for invalid alias")
	}

	testAlias = append(testAlias, "/dev/sda")
	if !isTestAlias("/dev/sda") {
		t.Fatalf("Should have returned true for valid alias")
	}

	testAlias = append(testAlias, "/dev/sdb")
	if !isTestAlias("/dev/sdb") {
		t.Fatalf("Should have returned true for valid alias")
	}
}

func TestBlockDevicesAlias(t *testing.T) {
	path := filepath.Join(testsDir, "block-devices-alias.yaml")
	model, err := LoadFile(path, args.Args{})

	if err != nil {
		t.Fatalf("Failed to load yaml file: %s", err)
	}

	tm := model.TargetMedias[0]

	if tm.Name != "sda" {
		t.Fatalf("Failed to expand Name variable, value: %s, expected: sda", tm.Name)
	}

	if tm.GetDeviceFile() != "/dev/sda" {
		t.Fatalf("Invalid device name value: %s, expected: /dev/sda", tm.GetDeviceFile())
	}

	for i, bd := range tm.Children {
		expected := fmt.Sprintf("sda%d", i+1)
		expectedFile := filepath.Join("/dev/", expected)

		if bd.Name != expected {
			t.Fatalf("Failed to expand Name variable, value: %s, expected: %s", bd.Name, expected)
		}

		if bd.GetDeviceFile() != expectedFile {
			t.Fatalf("Invalid device name value: %s, expected: %s", bd.GetDeviceFile(), expectedFile)
		}
	}
}

func TestBlockDevicesAliasOverwrite(t *testing.T) {
	path := filepath.Join(testsDir, "block-devices-alias.yaml")
	options := args.Args{BlockDevices: []string{"target:/dev/sdb"}}

	model, err := LoadFile(path, options)

	if err != nil {
		t.Fatalf("Failed to load yaml file: %s", err)
	}

	tm := model.TargetMedias[0]

	if tm.Name != "sdb" {
		t.Fatalf("Failed to expand Name variable, value: %s, expected: sdb", tm.Name)
	}

	if tm.GetDeviceFile() != "/dev/sdb" {
		t.Fatalf("Invalid device name value: %s, expected: /dev/sdb", tm.GetDeviceFile())
	}

	for i, bd := range tm.Children {
		expected := fmt.Sprintf("sdb%d", i+1)
		expectedFile := filepath.Join("/dev/", expected)

		if bd.Name != expected {
			t.Fatalf("Failed to expand Name variable, value: %s, expected: %s", bd.Name, expected)
		}

		if bd.GetDeviceFile() != expectedFile {
			t.Fatalf("Invalid device name value: %s, expected: %s", bd.GetDeviceFile(), expectedFile)
		}
	}
}

func TestInvalidBlockDeviceArgument(t *testing.T) {
	path := filepath.Join(testsDir, "block-devices-alias.yaml")
	options := args.Args{BlockDevices: []string{"invalid"}}

	model, err := LoadFile(path, options)

	if err != nil {
		t.Fatalf("Failed to load yaml file: %s", err)
	}

	if len(model.StorageAlias) != 1 {
		t.Fatalf("The model should contain only 2 storage aliases")
	}

	for _, curr := range model.StorageAlias {
		if curr.Name == "invalid" {
			t.Fatalf("The \"invalid\" block-device argument shouldn't be added to the model")
		}
	}
}

func TestEnableTelemetry(t *testing.T) {
	si := &SystemInstall{}

	if si.IsTelemetryEnabled() == true {
		t.Fatal("Default value for telemetry should be false")
	}

	// should always succeed
	si.EnableTelemetry(true)
	if si.Telemetry == nil {
		t.Fatal("SystemInstall.EnableTelemetry() should allocate Telemetry object")
	}

	if si.IsTelemetryEnabled() == false {
		t.Fatal("Wrong Telemetry value set or returned")
	}
}

func TestUnreadable(t *testing.T) {
	file, err := ioutil.TempFile("", "test-")
	if err != nil {
		t.Fatal("Could not create a temp file")
	}
	defer func() {
		if err = file.Close(); err != nil {
			t.Fatal(err)
		}
		_ = os.Remove(file.Name())
	}()

	if file.Chmod(0111) != nil {
		t.Fatal("Failed to change tmp file mod")
	}

	if utils.IsRoot() {
		t.Log("Not running as 'root', not checking read permission")
	} else {
		_, err = LoadFile(file.Name(), args.Args{})
		if err == nil {
			t.Fatal("Should have failed to read")
		}
	}
	if os.Remove(file.Name()) != nil {
		t.Fatal("Failed to cleanup test file")
	}
}

func TestBundle(t *testing.T) {
	si := &SystemInstall{}

	if si.ContainsBundle("test-bundle") {
		t.Fatal("Should return false since test-bundle wasn't added to si")
	}

	si.AddBundle("test-bundle")
	si.AddBundle("test-bundle-2")
	if !si.ContainsBundle("test-bundle") {
		t.Fatal("Should return true since test-bundle was added to si")
	}

	si.RemoveBundle("test-bundle")
	if si.ContainsBundle("test-bundle") {
		t.Fatal("Should return false since test-bundle was removed from si")
	}

	si.RemoveBundle("test-bundle-2")

	// duplicated
	si.AddBundle("test-bundle")
	si.AddBundle("test-bundle")
	if len(si.Bundles) > 1 {
		t.Fatal("We should have handled the duplication")
	}
}

func TestUserBundle(t *testing.T) {
	si := &SystemInstall{}

	if si.ContainsUserBundle("test-ubundle") {
		t.Fatal("Should return false since test-ubundle wasn't added to si")
	}

	si.AddUserBundle("test-ubundle")
	si.AddUserBundle("test-ubundle-2")
	if !si.ContainsUserBundle("test-ubundle") {
		t.Fatal("Should return true since test-ubundle was added to si")
	}

	si.RemoveUserBundle("test-ubundle")
	if si.ContainsUserBundle("test-ubundle") {
		t.Fatal("Should return false since test-ubundle was removed from si")
	}

	si.RemoveUserBundle("test-ubundle-2")

	// duplicated
	si.AddUserBundle("test-ubundle")
	si.AddUserBundle("test-ubundle")
	if len(si.UserBundles) > 1 {
		t.Fatal("We should have handled the duplication")
	}
}

func TestAddTargetMedia(t *testing.T) {
	path := filepath.Join(testsDir, "basic-valid-descriptor.yaml")
	loaded, err := LoadFile(path, args.Args{})

	if err != nil {
		t.Fatal("Failed to load a valid descriptor")
	}

	nm := &SystemInstall{}
	nm.AddTargetMedia(loaded.TargetMedias[0])
	if len(nm.TargetMedias) != 1 {
		t.Fatal("Failed to add target media to model")
	}

	// the AddTargetMedia() interface must prevent duplication
	cl := len(nm.TargetMedias)
	nm.AddTargetMedia(loaded.TargetMedias[0])
	if len(nm.TargetMedias) != cl {
		t.Fatal("AddTargetMedia() must prevent duplication")
	}

	// AddTargetMedia() should always add non equal medias
	clone := loaded.TargetMedias[0].Clone()
	clone.Name = clone.Name + "-cloned"

	// Check for encryption passphrase needed; should not
	if nm.EncryptionRequiresPassphrase(false) {
		t.Fatal("nm.EncryptionRequiresPassphrase() should NOT be true")
	}

	nm.AddTargetMedia(clone)
	if len(nm.TargetMedias) == cl {
		t.Fatal("AddTargetMedia() failed to add a cloned and modified target media")
	}
}

func TestAddNetworkInterface(t *testing.T) {
	path := filepath.Join(testsDir, "valid-network.yaml")
	loaded, err := LoadFile(path, args.Args{})

	if err != nil {
		t.Fatal("Failed to load a valid descriptor")
	}

	nm := &SystemInstall{}
	nm.AddNetworkInterface(loaded.NetworkInterfaces[0])
	if len(nm.NetworkInterfaces) != 1 {
		t.Fatal("Failed to add network interface to model")
	}
}

func TestDesktopISOType(t *testing.T) {
	path := filepath.Join(testsDir, "iso-desktop.yaml")
	loaded, err := LoadFile(path, args.Args{})

	loaded.AddUserBundle("user-basic")
	loaded.AddUserBundle("python3-basic")

	if err != nil {
		t.Fatal("Failed to load a valid descriptor")
	}

	if !loaded.IsTargetDesktopInstall() {
		t.Fatal("Failed to detect Desktop ISO install from model")
	}
}

func TestDesktopUserISOType(t *testing.T) {
	path := filepath.Join(testsDir, "iso-good.yaml")
	loaded, err := LoadFile(path, args.Args{})

	loaded.AddUserBundle("user-basic")
	loaded.AddUserBundle("python3-basic")
	loaded.AddUserBundle("desktop-apps")

	if err != nil {
		t.Fatal("Failed to load a valid descriptor")
	}

	if !loaded.IsTargetDesktopInstall() {
		t.Fatal("Failed to detect Desktop ISO install from model")
	}
}

func TestServerISOType(t *testing.T) {
	path := filepath.Join(testsDir, "iso-good.yaml")
	loaded, err := LoadFile(path, args.Args{})

	loaded.AddUserBundle("user-basic")
	loaded.AddUserBundle("python3-basic")

	if err != nil {
		t.Fatal("Failed to load a valid descriptor")
	}

	if loaded.IsTargetDesktopInstall() {
		t.Fatal("Detected Desktop ISO install, but should be Server from model")
	}
}

func TestUser(t *testing.T) {
	users := []*user.User{
		{Login: "login1", Password: "pwd1", Admin: false},
		{Login: "login2", Password: "pwd2", Admin: false},
		{Login: "login3", Password: "pwd3", Admin: false},
		{Login: "login4", Password: "pwd4", Admin: false},
	}

	si := &SystemInstall{}

	for i, curr := range users {
		si.AddUser(curr)

		if len(si.Users) != i+1 {
			t.Fatal("User wasn't added")
		}
	}

	cl := len(si.Users)

	// don't add same user twice
	si.AddUser(users[0])
	if len(si.Users) != cl {
		t.Fatal("The AddUser() interface should prevent user duplication")
	}

	si.RemoveAllUsers()
	if len(si.Users) != 0 {
		t.Fatal("User list should be empty")
	}
}

func TestWriteFile(t *testing.T) {
	path := filepath.Join(testsDir, "basic-valid-descriptor.yaml")
	loaded, err := LoadFile(path, args.Args{})

	if err != nil {
		t.Fatal("Failed to load a valid descriptor")
	}

	tmpFile, err := ioutil.TempFile("", "test-")
	if err != nil {
		t.Fatal("Could not create a temp file")
	}
	defer func() { _ = os.Remove(tmpFile.Name()) }()
	path = tmpFile.Name()
	if err = tmpFile.Close(); err != nil {
		t.Fatal(err)
	}

	if err := loaded.WriteFile(path); err != nil {
		t.Fatal("Failed to write descriptor, should be valid")
	}

	// test writing to an invalid file
	if err := loaded.WriteFile("/invalid-dir/invalid.yaml"); err == nil {
		t.Fatal("Should have failed writing to an invalid file")
	}
}

func TestAddExtraKernelArguments(t *testing.T) {
	args := []string{"arg1", "arg2", "arg3"}

	si := &SystemInstall{}
	si.AddExtraKernelArguments(args)

	if si.KernelArguments == nil {
		t.Fatal("AddExtraKernelArguments() should had created a KernelArguments object")
	}

	if len(si.KernelArguments.Add) != len(args) {
		t.Fatal("AddExtraKernelArguments() didn't add all requested arguments")
	}

	for _, curr := range args {
		if !utils.StringSliceContains(si.KernelArguments.Add, curr) {
			t.Fatal("AddExtraKernelArguments() didn't add all the requested arguments")
		}
	}

	l := len(si.KernelArguments.Add)

	// testing duplication checks
	si.AddExtraKernelArguments(args)

	if l < len(si.KernelArguments.Add) {
		t.Fatal("The duplication check has failed")
	}
}

func TestRemoveKernelArguments(t *testing.T) {
	args := []string{"arg1", "arg2", "arg3"}

	si := &SystemInstall{}
	si.RemoveKernelArguments(args)

	if si.KernelArguments == nil {
		t.Fatal("RemoveKernelArguments() should had created a KernelArguments object")
	}

	if len(si.KernelArguments.Remove) != len(args) {
		t.Fatal("RemoveKernelArguments() didn't add all requested arguments")
	}

	for _, curr := range args {
		if !utils.StringSliceContains(si.KernelArguments.Remove, curr) {
			t.Fatal("RemoveKernelArguments() didn't add all the requested arguments")
		}
	}

	l := len(si.KernelArguments.Remove)

	// testing duplication check
	si.RemoveKernelArguments(args)

	if l < len(si.KernelArguments.Remove) {
		t.Fatal("The duplication check has failed")
	}
}

func TestClearExtraKernelArguments(t *testing.T) {
	args := []string{"arg1", "arg2", "arg3"}

	si := &SystemInstall{}
	si.AddExtraKernelArguments(args)

	si.ClearExtraKernelArguments()

	l := len(si.KernelArguments.Add)

	if l > 0 {
		t.Fatal("Extra Add arguments failed to be cleared")
	}
}

func TestClearRemoveKernelArguments(t *testing.T) {
	args := []string{"arg1", "arg2", "arg3"}

	si := &SystemInstall{}
	si.RemoveKernelArguments(args)

	si.ClearRemoveKernelArguments()

	l := len(si.KernelArguments.Remove)

	if l > len(si.KernelArguments.Remove) {
		t.Fatal("Remove arguments failed to be cleared")
	}
}

func TestAddEncryptedTargetMedia(t *testing.T) {
	path := filepath.Join(testsDir, "encrypt-valid-descriptor.yaml")
	loaded, err := LoadFile(path, args.Args{})

	if err != nil {
		t.Fatal("Failed to load a valid descriptor")
	}

	nm := &SystemInstall{}
	nm.AddTargetMedia(loaded.TargetMedias[0])
	if len(nm.TargetMedias) != 1 {
		t.Fatal("Failed to add target media to model")
	}
	cl := len(nm.TargetMedias)

	// AddTargetMedia() should always add non equal medias
	clone := loaded.TargetMedias[0].Clone()
	clone.Name = clone.Name + "-cloned"

	// Check for encryption passphrase needed; should not
	if !nm.EncryptionRequiresPassphrase(false) {
		t.Fatal("nm.EncryptionRequiresPassphrase() must always be true")
	}

	nm.AddTargetMedia(clone)
	if len(nm.TargetMedias) == cl {
		t.Fatal("AddTargetMedia() failed to add a cloned and modified target media")
	}
}

func TestBackupFile(t *testing.T) {
	var err error
	path := filepath.Join(testsDir, "valid-ister-full-physical.json")
	cf := strings.TrimSuffix(path, filepath.Ext(path)) + ".yaml"
	md, err := JSONtoYAMLConfig(path)
	if err == nil {
		_, _ = md.WriteYAMLConfig(cf)
		defer func() { _ = os.Remove(cf) }()
	}

	info, err := os.Stat(cf)
	if os.IsNotExist(err) {
		t.Fatalf("%s should already exist and shouldn't return an error: %v", cf, err)
	}

	mt := info.ModTime()
	suffix := fmt.Sprintf("-%d-%02d-%02d-%02d%02d%02d",
		mt.Year(), mt.Month(), mt.Day(),
		mt.Hour(), mt.Minute(), mt.Second())
	bf := strings.TrimSuffix(cf, filepath.Ext(cf)) + suffix + ".yaml"

	md2, err := JSONtoYAMLConfig(path)
	if err == nil {
		defer func() {
			_ = os.Remove(path)
			_ = os.Remove(bf)
		}()
		path, err = md2.WriteYAMLConfig(path)
	}

	if err != nil {
		t.Fatalf("%s is a valid test and shouldn't return an error: %v", path, err)
	}
	info, err = os.Stat(cf)
	if os.IsNotExist(err) {
		t.Fatalf("%s should still exist and shouldn't return an error: %v", cf, err)
	}
	info, err = os.Stat(bf)
	if os.IsNotExist(err) {
		t.Fatalf("%s should exist and shouldn't return an error: %v", cf, err)
	}
}

func TestWriteScrubModelTargetMedias(t *testing.T) {
	si := &SystemInstall{}
	yaml, err := si.WriteScrubModelTargetMedias()
	defer func() { _ = os.Remove(yaml) }()
	if err != nil {
		t.Fatalf("WriteScrubModelTargetMedias shouldn't return an error: %v", err)
	}
}

func TestClearInstallSelected(t *testing.T) {
	si := &SystemInstall{}
	si.ClearInstallSelected()

	si.InstallSelected["name"] = storage.InstallTarget{Name: "name", Friendly: "friendly"}

	if si.InstallSelected["name"].Friendly != "friendly" {
		t.Fatalf("Name %s should exist with friendly as friendly", "name")
	}

	si.ClearInstallSelected()
	if si.InstallSelected["name"].Friendly == "friendly" {
		t.Fatalf("Name %s should NOT exist with friendly as friendly", "name")
	}
}

func TestInterActiveOfflineFail(t *testing.T) {
	si := &SystemInstall{}
	si.ClearInstallSelected()

	si.Offline = true

	if err := si.InteractiveOptionsValid(); err == nil {
		t.Fatalf("Interactive should fail with Offline set to true")
	}
}
func TestInterActiveIsoFail(t *testing.T) {
	si := &SystemInstall{}
	si.ClearInstallSelected()

	si.MakeISO = true

	if err := si.InteractiveOptionsValid(); err == nil {
		t.Fatalf("Interactive should fail with ISO set to true")
	}
}
func TestInterActivePass(t *testing.T) {
	si := &SystemInstall{}
	si.ClearInstallSelected()

	si.Offline = false
	si.MakeISO = false

	if err := si.InteractiveOptionsValid(); err != nil {
		t.Fatalf("Interactive should pass: %v", err)
	}
}

func TestInitDefaults(t *testing.T) {
	si := &SystemInstall{}

	// Exercise both code paths
	// First time pointers are nil, second time they have a value
	for i := 1; i < 2; i++ {
		si.InitializeDefaults()
		if si.PostArchive == nil {
			t.Fatalf("InitializeDefaults failed to initialize PostArchive default")
		}
		if !si.PostArchive.IsDefault() {
			t.Fatalf("InitializeDefaults failed to set PostArchive default")
		}

		if si.AutoUpdate == nil {
			t.Fatalf("InitializeDefaults failed to initialize AutoUpdate default")
		}
		if !si.AutoUpdate.IsDefault() {
			t.Fatalf("InitializeDefaults failed to set AutoUpdate default")
		}
	}
}

func TestSetDefaultSwapFilePass(t *testing.T) {
	si := &SystemInstall{}
	if si.MediaOpts.SwapFileSize != "" {
		t.Fatalf("Default SwapFileSize should be empty: %q", si.MediaOpts.SwapFileSize)
	}

	si.SetDefaultSwapFileSize()
	if si.MediaOpts.SwapFileSize == "" {
		t.Fatalf("SwapFileSize should be string version of %d", storage.SwapFileSizeDefault)
	}

	testValue := "invalid-but-legal"
	si.MediaOpts.SwapFileSize = testValue
	si.SetDefaultSwapFileSize()
	if si.MediaOpts.SwapFileSize != testValue {
		t.Fatalf("SwapFileSize should be string %q", testValue)
	}

	si.ResetDefaultSwapFileSize()
	if si.MediaOpts.SwapFileSize != "" {
		t.Fatalf("SwapFileSize should be clear to empty string, not %q", si.MediaOpts.SwapFileSize)
	}

	testValue = "4G"
	si.MediaOpts.SwapFileSize = testValue
	si.MediaOpts.SwapFileSet = true
	si.ResetDefaultSwapFileSize()
	if si.MediaOpts.SwapFileSize == "" {
		t.Fatalf("SwapFileSize should be set to %q, but it is empty", testValue)
	}
}

func TestJSONUnmarshalIster(t *testing.T) {
	var us IsterConfig

	if err := json.Unmarshal([]byte(`{"Version" "latest"}`), &us); err == nil {
		t.Fatalf("UnmarshalJSON should have had an error")
	} else {
		t.Logf("Found expected error: %+v", err)
	}

	if err := json.Unmarshal([]byte(`{"Version": 0.567}`), &us); err == nil {
		t.Fatalf("UnmarshalJSON should have had an error")
	} else {
		t.Logf("Found expected error: %+v", err)
	}

	if err := json.Unmarshal([]byte(`{"Version": "latest"}`), &us); err != nil {
		t.Fatalf("UnmarshalJSON error %+v", err)
	}
	if us.Version.Number != 0 {
		t.Fatalf("Version latest should always be 0")
	}

	if err := json.Unmarshal([]byte(`{"Version": ""}`), &us); err != nil {
		t.Fatalf("UnmarshalJSON error %+v", err)
	}
	if us.Version.Number != 0 {
		t.Fatalf("Version '' should always be 0")
	}

	if err := json.Unmarshal([]byte(`{"Version": 0}`), &us); err != nil {
		t.Fatalf("UnmarshalJSON error %+v", err)
	}
	if us.Version.Number != 0 {
		t.Fatalf("Version '0' should always be 0, not %d", us.Version.Number)
	}

	if err := json.Unmarshal([]byte(`{"Version": "10"}`), &us); err != nil {
		t.Fatalf("UnmarshalJSON error %+v", err)
	}
	if us.Version.Number != 10 {
		t.Fatalf("Version \"10\" should always be 10, not %d", us.Version.Number)
	}

	if err := json.Unmarshal([]byte(`{"Version": 10}`), &us); err != nil {
		t.Fatalf("UnmarshalJSON error %+v", err)
	}
	if us.Version.Number != 10 {
		t.Fatalf("Version 10 should always be 10, not %d", us.Version.Number)
	}

	if err := json.Unmarshal([]byte(`{"Version": "54321"}`), &us); err != nil {
		t.Fatalf("UnmarshalJSON error %+v", err)
	}
	if us.Version.Number != 54321 {
		t.Fatalf("Version \"54321\" should always be 54321, not %d", us.Version.Number)
	}

	if err := json.Unmarshal([]byte(`{"Version": 54321}`), &us); err != nil {
		t.Fatalf("UnmarshalJSON error %+v", err)
	}
	if us.Version.Number != 54321 {
		t.Fatalf("Version 54321 should always be 54321, not %d", us.Version.Number)
	}
}
