package cli

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"testing"

	"github.com/ipfs/boxo/ipns"
	"github.com/ipfs/kubo/core/commands/name"
	"github.com/ipfs/kubo/test/cli/harness"
	"github.com/stretchr/testify/require"
)

func TestName(t *testing.T) {
	const (
		fixturePath = "fixtures/TestName.car"
		fixtureCid  = "bafybeidg3uxibfrt7uqh7zd5yaodetik7wjwi4u7rwv2ndbgj6ec7lsv2a"
		dagCid      = "bafyreidgts62p4rtg3rtmptmbv2dt46zjzin275fr763oku3wfod3quzay"
	)

	makeDaemon := func(t *testing.T, initArgs []string) *harness.Node {
		node := harness.NewT(t).NewNode().Init(append([]string{"--profile=test"}, initArgs...)...)
		r, err := os.Open(fixturePath)
		require.Nil(t, err)
		defer r.Close()
		err = node.IPFSDagImport(r, fixtureCid)
		require.NoError(t, err)
		return node
	}

	testPublishingWithSelf := func(keyType string) {
		t.Run("Publishing with self (keyType = "+keyType+")", func(t *testing.T) {
			t.Parallel()

			args := []string{}
			if keyType != "default" {
				args = append(args, "-a="+keyType)
			}

			node := makeDaemon(t, args)
			name := ipns.NameFromPeer(node.PeerID())

			t.Run("Publishing a CID", func(t *testing.T) {
				publishPath := "/ipfs/" + fixtureCid

				res := node.IPFS("name", "publish", "--allow-offline", publishPath)
				require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())

				res = node.IPFS("name", "resolve", "/ipns/"+name.String())
				require.Equal(t, publishPath+"\n", res.Stdout.String())
			})

			t.Run("Publishing a CID with -Q option", func(t *testing.T) {
				publishPath := "/ipfs/" + fixtureCid

				res := node.IPFS("name", "publish", "--allow-offline", "-Q", publishPath)
				require.Equal(t, name.String()+"\n", res.Stdout.String())

				res = node.IPFS("name", "resolve", "/ipns/"+name.String())
				require.Equal(t, publishPath+"\n", res.Stdout.String())
			})

			t.Run("Publishing a CID+subpath", func(t *testing.T) {
				publishPath := "/ipfs/" + fixtureCid + "/hello"

				res := node.IPFS("name", "publish", "--allow-offline", publishPath)
				require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())

				res = node.IPFS("name", "resolve", "/ipns/"+name.String())
				require.Equal(t, publishPath+"\n", res.Stdout.String())
			})

			t.Run("Publishing nothing fails", func(t *testing.T) {
				res := node.RunIPFS("name", "publish")
				require.Error(t, res.Err)
				require.Equal(t, 1, res.ExitCode())
				require.Contains(t, res.Stderr.String(), `argument "ipfs-path" is required`)
			})

			t.Run("Publishing with IPLD works", func(t *testing.T) {
				publishPath := "/ipld/" + dagCid + "/thing"
				res := node.IPFS("name", "publish", "--allow-offline", publishPath)
				require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())

				res = node.IPFS("name", "resolve", "/ipns/"+name.String())
				require.Equal(t, publishPath+"\n", res.Stdout.String())
			})

			publishPath := "/ipfs/" + fixtureCid
			res := node.IPFS("name", "publish", "--allow-offline", publishPath)
			require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())

			t.Run("Resolving self offline succeeds (daemon off)", func(t *testing.T) {
				res = node.IPFS("name", "resolve", "--offline", "/ipns/"+name.String())
				require.Equal(t, publishPath+"\n", res.Stdout.String())

				// Test without cache.
				res = node.IPFS("name", "resolve", "--offline", "-n", "/ipns/"+name.String())
				require.Equal(t, publishPath+"\n", res.Stdout.String())
			})

			node.StartDaemon()

			t.Run("Resolving self offline succeeds (daemon on)", func(t *testing.T) {
				res = node.IPFS("name", "resolve", "--offline", "/ipns/"+name.String())
				require.Equal(t, publishPath+"\n", res.Stdout.String())

				// Test without cache.
				res = node.IPFS("name", "resolve", "--offline", "-n", "/ipns/"+name.String())
				require.Equal(t, publishPath+"\n", res.Stdout.String())
			})
		})
	}

	testPublishingWithSelf("default")
	testPublishingWithSelf("rsa")
	testPublishingWithSelf("ed25519")

	testPublishWithKey := func(name string, keyArgs ...string) {
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			node := makeDaemon(t, nil)

			keyGenArgs := []string{"key", "gen"}
			keyGenArgs = append(keyGenArgs, keyArgs...)
			keyGenArgs = append(keyGenArgs, "key")

			res := node.IPFS(keyGenArgs...)
			key := strings.TrimSpace(res.Stdout.String())

			publishPath := "/ipfs/" + fixtureCid
			name, err := ipns.NameFromString(key)
			require.NoError(t, err)

			res = node.IPFS("name", "publish", "--allow-offline", "--key="+key, publishPath)
			require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
		})
	}

	testPublishWithKey("Publishing with RSA (with b58mh) Key", "--ipns-base=b58mh", "--type=rsa", "--size=2048")
	testPublishWithKey("Publishing with ED25519 (with b58mh) Key", "--ipns-base=b58mh", "--type=ed25519")
	testPublishWithKey("Publishing with ED25519 (with base36) Key", "--ipns-base=base36", "--type=ed25519")

	t.Run("Fails to publish in offline mode", func(t *testing.T) {
		t.Parallel()
		node := makeDaemon(t, nil).StartDaemon("--offline")
		res := node.RunIPFS("name", "publish", "/ipfs/"+fixtureCid)
		require.Error(t, res.Err)
		require.Equal(t, 1, res.ExitCode())
		require.Contains(t, res.Stderr.String(), "can't publish while offline: pass `--allow-offline` to override or `--allow-delegated` if Ipns.DelegatedPublishers are set up")
	})

	t.Run("Publish V2-only record", func(t *testing.T) {
		t.Parallel()

		node := makeDaemon(t, nil).StartDaemon()
		ipnsName := ipns.NameFromPeer(node.PeerID()).String()
		ipnsPath := ipns.NamespacePrefix + ipnsName
		publishPath := "/ipfs/" + fixtureCid

		res := node.IPFS("name", "publish", "--ttl=30m", "--v1compat=false", publishPath)
		require.Equal(t, fmt.Sprintf("Published to %s: %s\n", ipnsName, publishPath), res.Stdout.String())

		res = node.IPFS("name", "resolve", ipnsPath)
		require.Equal(t, publishPath+"\n", res.Stdout.String())

		res = node.IPFS("routing", "get", ipnsPath)
		record := res.Stdout.Bytes()

		res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect")
		out := res.Stdout.String()
		require.Contains(t, out, "This record was not verified.")
		require.Contains(t, out, publishPath)
		require.Contains(t, out, "30m")

		res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--verify="+ipnsPath)
		out = res.Stdout.String()
		require.Contains(t, out, "Valid: true")
		require.Contains(t, out, "Signature Type: V2")
		require.Contains(t, out, fmt.Sprintf("Protobuf Size:  %d", len(record)))
	})

	t.Run("Publish with TTL and inspect record", func(t *testing.T) {
		t.Parallel()

		node := makeDaemon(t, nil).StartDaemon()
		ipnsPath := ipns.NamespacePrefix + ipns.NameFromPeer(node.PeerID()).String()
		publishPath := "/ipfs/" + fixtureCid

		_ = node.IPFS("name", "publish", "--ttl=30m", publishPath)
		res := node.IPFS("routing", "get", ipnsPath)
		record := res.Stdout.Bytes()

		t.Run("Inspect record shows correct TTL and that it is not validated", func(t *testing.T) {
			t.Parallel()
			res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect")
			out := res.Stdout.String()
			require.Contains(t, out, "This record was not verified.")
			require.Contains(t, out, publishPath)
			require.Contains(t, out, "30m")
			require.Contains(t, out, "Signature Type: V1+V2")
			require.Contains(t, out, fmt.Sprintf("Protobuf Size:  %d", len(record)))
		})

		t.Run("Inspect record shows valid with correct name", func(t *testing.T) {
			t.Parallel()
			res := node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--enc=json", "--verify="+ipnsPath)
			val := name.IpnsInspectResult{}
			err := json.Unmarshal(res.Stdout.Bytes(), &val)
			require.NoError(t, err)
			require.True(t, val.Validation.Valid)
		})

		t.Run("Inspect record shows invalid with wrong name", func(t *testing.T) {
			t.Parallel()
			res := node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--enc=json", "--verify=12D3KooWRirYjmmQATx2kgHBfky6DADsLP7ex1t7BRxJ6nqLs9WH")
			val := name.IpnsInspectResult{}
			err := json.Unmarshal(res.Stdout.Bytes(), &val)
			require.NoError(t, err)
			require.False(t, val.Validation.Valid)
		})
	})

	t.Run("Inspect with verification using wrong RSA key errors", func(t *testing.T) {
		t.Parallel()
		node := makeDaemon(t, nil).StartDaemon()

		// Prepare RSA Key 1
		res := node.IPFS("key", "gen", "--type=rsa", "--size=4096", "key1")
		key1 := strings.TrimSpace(res.Stdout.String())
		name1, err := ipns.NameFromString(key1)
		require.NoError(t, err)

		// Prepare RSA Key 2
		res = node.IPFS("key", "gen", "--type=rsa", "--size=4096", "key2")
		key2 := strings.TrimSpace(res.Stdout.String())
		name2, err := ipns.NameFromString(key2)
		require.NoError(t, err)

		// Publish using Key 1
		publishPath := "/ipfs/" + fixtureCid
		res = node.IPFS("name", "publish", "--allow-offline", "--key="+key1, publishPath)
		require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name1.String(), publishPath), res.Stdout.String())

		// Get IPNS Record
		res = node.IPFS("routing", "get", ipns.NamespacePrefix+name1.String())
		record := res.Stdout.Bytes()

		// Validate with correct key succeeds
		res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--verify="+name1.String(), "--enc=json")
		val := name.IpnsInspectResult{}
		err = json.Unmarshal(res.Stdout.Bytes(), &val)
		require.NoError(t, err)
		require.True(t, val.Validation.Valid)

		// Validate with wrong key fails
		res = node.PipeToIPFS(bytes.NewReader(record), "name", "inspect", "--verify="+name2.String(), "--enc=json")
		val = name.IpnsInspectResult{}
		err = json.Unmarshal(res.Stdout.Bytes(), &val)
		require.NoError(t, err)
		require.False(t, val.Validation.Valid)
	})

	t.Run("Publishing with custom sequence number", func(t *testing.T) {
		t.Parallel()

		node := makeDaemon(t, nil)
		publishPath := "/ipfs/" + fixtureCid
		name := ipns.NameFromPeer(node.PeerID())

		t.Run("Publish with sequence=0 is not allowed", func(t *testing.T) {
			// Sequence=0 is never valid, even on a fresh node
			res := node.RunIPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=0", publishPath)
			require.NotEqual(t, 0, res.ExitCode(), "Expected publish with sequence=0 to fail")
			require.Contains(t, res.Stderr.String(), "sequence number must be greater than the current record sequence")
		})

		t.Run("Publish with sequence=1 on fresh node", func(t *testing.T) {
			// Sequence=1 is the minimum valid sequence number for first publish
			res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=1", publishPath)
			require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
		})

		t.Run("Publish with sequence=42", func(t *testing.T) {
			res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=42", publishPath)
			require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
		})

		t.Run("Publish with large sequence number", func(t *testing.T) {
			res := node.IPFS("name", "publish", "--allow-offline", "--ttl=0", "--sequence=18446744073709551615", publishPath) // Max uint64
			require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath), res.Stdout.String())
		})
	})

	t.Run("Sequence number monotonic check", func(t *testing.T) {
		t.Parallel()

		node := makeDaemon(t, nil).StartDaemon()
		publishPath1 := "/ipfs/" + fixtureCid
		publishPath2 := "/ipfs/" + dagCid // Different content
		name := ipns.NameFromPeer(node.PeerID())

		// First, publish with a high sequence number (1000)
		res := node.IPFS("name", "publish", "--ttl=0", "--sequence=1000", publishPath1)
		require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath1), res.Stdout.String())

		// Verify the record was published successfully
		res = node.IPFS("name", "resolve", name.String())
		require.Contains(t, res.Stdout.String(), publishPath1)

		// Now try to publish different content with a LOWER sequence number (500)
		// This should fail due to monotonic sequence check
		res = node.RunIPFS("name", "publish", "--ttl=0", "--sequence=500", publishPath2)
		require.NotEqual(t, 0, res.ExitCode(), "Expected publish with lower sequence to fail")
		require.Contains(t, res.Stderr.String(), "sequence number", "Expected error about sequence number")

		// Verify the original content is still published (not overwritten)
		res = node.IPFS("name", "resolve", name.String())
		require.Contains(t, res.Stdout.String(), publishPath1, "Original content should still be published")
		require.NotContains(t, res.Stdout.String(), publishPath2, "New content should not have been published")

		// Publishing with a HIGHER sequence number should succeed
		res = node.IPFS("name", "publish", "--ttl=0", "--sequence=2000", publishPath2)
		require.Equal(t, fmt.Sprintf("Published to %s: %s\n", name.String(), publishPath2), res.Stdout.String())

		// Verify the new content is now published
		res = node.IPFS("name", "resolve", name.String())
		require.Contains(t, res.Stdout.String(), publishPath2, "New content should now be published")
	})
}
