package chain_test

import (
	"bytes"
	"fmt"
	"net"
	"net/http"
	"os"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"

	chainconfig "github.com/ignite/cli/v29/ignite/config/chain"
	"github.com/ignite/cli/v29/ignite/config/chain/version"
	"github.com/ignite/cli/v29/ignite/config/testdata"
	"github.com/ignite/cli/v29/ignite/pkg/availableport"
	"github.com/ignite/cli/v29/ignite/pkg/errors"
)

func TestReadConfigVersion(t *testing.T) {
	// Arrange
	r := strings.NewReader("version: 42")
	want := version.Version(42)

	// Act
	v, err := chainconfig.ReadConfigVersion(r)

	// Assert
	require.NoError(t, err)
	require.Equal(t, want, v)
}

func TestParse(t *testing.T) {
	// Arrange: Initialize a reader with the previous version
	ver := chainconfig.LatestVersion - 1
	r := bytes.NewReader(testdata.Versions[ver])

	// Act
	cfg, err := chainconfig.Parse(r)

	// Assert
	require.NoError(t, err)

	// Assert: Parse must return the latest version
	require.Equal(t, chainconfig.LatestVersion, cfg.Version)
	require.Equal(t, testdata.GetLatestConfig(t), cfg)
}

func TestParseWithCurrentVersion(t *testing.T) {
	// Arrange
	r := bytes.NewReader(testdata.Versions[chainconfig.LatestVersion])

	// Act
	cfg, err := chainconfig.Parse(r)

	// Assert
	require.NoError(t, err)
	require.Equal(t, chainconfig.LatestVersion, cfg.Version)
	require.Equal(t, testdata.GetLatestConfig(t), cfg)
}

func TestParseWithUnknownVersion(t *testing.T) {
	// Arrange
	version := version.Version(9999)
	r := strings.NewReader(fmt.Sprintf("version: %d", version))

	var want *chainconfig.UnsupportedVersionError

	// Act
	_, err := chainconfig.Parse(r)

	// Assert
	require.ErrorAs(t, err, &want)
	require.NotNil(t, want)
	require.Equal(t, want.Version, version)
}

func TestParseNetworkWithCurrentVersion(t *testing.T) {
	// Arrange
	r := bytes.NewReader(testdata.NetworkConfig)

	// Act
	cfg, err := chainconfig.ParseNetwork(r)

	// Assert
	require.NoError(t, err)

	// Assert: Parse must return the latest version
	require.Equal(t, chainconfig.LatestVersion, cfg.Version)
	require.Equal(t, testdata.GetLatestNetworkConfig(t).Accounts, cfg.Accounts)
	require.Equal(t, testdata.GetLatestNetworkConfig(t).Genesis, cfg.Genesis)
}

func TestParseNetworkWithInvalidData(t *testing.T) {
	// Arrange
	r := bytes.NewReader(testdata.Versions[chainconfig.LatestVersion])

	// Act
	_, err := chainconfig.ParseNetwork(r)

	// Assert error
	require.True(
		t,
		strings.Contains(
			err.Error(),
			"config is not valid: no validators can be used in config for network genesis",
		),
	)
}

func TestHandleIncludes(t *testing.T) {
	server := startTestServer(t)

	tests := []struct {
		name       string
		baseConfig string
		expected   string
		err        error
	}{
		{
			name: "Single valid include",
			baseConfig: `
version: 1
client:
  typescript:
    path: original
include:
  - "./testdata/include1.yml"
`,

			expected: `
include:
    - ./testdata/include1.yml
validation: sovereign
version: 1
build:
    proto:
        path: proto
accounts:
    - name: bob
      coins:
        - 10000token
        - 100000000stake
faucet:
    name: danilo
    coins:
        - 5token
        - 100000stake
    host: 0.0.0.0:4500
client:
    typescript:
        path: override-1
    openapi:
        path: docs/static/include1.yml
        exclude_list: []
validators:
    - name: alice
      bonded: 100000000stake`,
		},
		{
			name: "Multiple valid includes",
			baseConfig: `
version: 1
client:
  typescript:
    path: original
include:
  - "./testdata/include1.yml"
  - "./testdata/include2.yml"
`,
			expected: `
include:
    - ./testdata/include1.yml
    - ./testdata/include2.yml
validation: sovereign
version: 1
build:
    proto:
        path: proto
accounts:
    - name: bob
      coins:
        - 10000token
        - 100000000stake
    - name: alice
      coins:
        - 20000token
        - 200000000stake
faucet:
    name: alice
    coins:
        - 5token
        - 100000stake
        - 5token
        - 100000stake
    host: 0.0.0.0:4500
client:
    typescript:
        path: override-2
    openapi:
        path: docs/static/include2.yml
        exclude_list: []
validators:
    - name: alice
      bonded: 100000000stake
    - name: validator1
      bonded: 100000000stake`,
		},
		{
			name: "Invalid include file path",
			baseConfig: `
version: 1
client:
  typescript:
    path: original
include:
  - "./testdata/nonexistent.yml"`,
			err: errors.New("error parsing config file: failed to open included file './testdata/nonexistent.yml'"),
		},
		{
			name: "Empty include list",
			baseConfig: `
version: 1
validation: sovereign
accounts:
  - name: alice
    coins:
      - 10000token
include: []
faucet:
  name: alice
  coins:
    - 5token
client:
  typescript:
    path: override-1
  openapi:
    path: docs/static/include1.yml
validators:
  - name: alice
    bonded: 100stake`,
			expected: `
validation: sovereign
version: 1
build:
    proto:
        path: proto
accounts:
    - name: alice
      coins:
        - 10000token
faucet:
    name: alice
    coins:
        - 5token
    host: 0.0.0.0:4500
client:
    typescript:
        path: override-1
    openapi:
        path: docs/static/include1.yml
        exclude_list: []
validators:
    - name: alice
      bonded: 100stake`,
		},
		{
			name: "Empty values include",
			baseConfig: `
version: 1
include:
  - "./testdata/include1.yml"
  - "./testdata/include2.yml"
`,
			expected: `
include:
    - ./testdata/include1.yml
    - ./testdata/include2.yml
validation: sovereign
version: 1
build:
    proto:
        path: proto
accounts:
    - name: bob
      coins:
        - 10000token
        - 100000000stake
    - name: alice
      coins:
        - 20000token
        - 200000000stake
faucet:
    name: alice
    coins:
        - 5token
        - 100000stake
        - 5token
        - 100000stake
    host: 0.0.0.0:4500
client:
    typescript:
        path: override-2
    openapi:
        path: docs/static/include2.yml
        exclude_list: []
validators:
    - name: alice
      bonded: 100000000stake
    - name: validator1
      bonded: 100000000stake`,
		},
		{
			name: "HTTP include",
			baseConfig: fmt.Sprintf(`
version: 1
validation: sovereign
accounts:
  - name: alice
    coins:
      - 10000token
include:
    - %[1]v/include1.yml
    - %[1]v/include2.yml
faucet:
  name: alice
  coins:
    - 5token
client:
  typescript:
    path: original
  openapi:
    path: docs/static/include1.yml
validators:
  - name: alice
    bonded: 100stake
`, server),
			expected: fmt.Sprintf(`
include:
    - %[1]v/include1.yml
    - %[1]v/include2.yml
validation: sovereign
version: 1
build:
    proto:
        path: proto
accounts:
    - name: alice
      coins:
        - 10000token
    - name: bob
      coins:
        - 10000token
        - 100000000stake
    - name: alice
      coins:
        - 20000token
        - 200000000stake
faucet:
    name: alice
    coins:
        - 5token
        - 5token
        - 100000stake
        - 5token
        - 100000stake
    host: 0.0.0.0:4500
client:
    typescript:
        path: override-2
    openapi:
        path: docs/static/include2.yml
        exclude_list: []
validators:
    - name: alice
      bonded: 100stake
    - name: alice
      bonded: 100000000stake
    - name: validator1
      bonded: 100000000stake`, server),
		},
		{
			name: "HTTP and local include",
			baseConfig: fmt.Sprintf(`
version: 1
validation: sovereign
accounts:
  - name: alice
    coins:
      - 10000token
include:
    - %[1]v/include1.yml
    - testdata/include2.yml
faucet:
  name: alice
  coins:
    - 5token
client:
  typescript:
    path: original
  openapi:
    path: docs/static/include1.yml
validators:
  - name: alice
    bonded: 100stake
`, server),
			expected: fmt.Sprintf(`
include:
    - %[1]v/include1.yml
    - testdata/include2.yml
validation: sovereign
version: 1
build:
    proto:
        path: proto
accounts:
    - name: alice
      coins:
        - 10000token
    - name: bob
      coins:
        - 10000token
        - 100000000stake
    - name: alice
      coins:
        - 20000token
        - 200000000stake
faucet:
    name: alice
    coins:
        - 5token
        - 5token
        - 100000stake
        - 5token
        - 100000stake
    host: 0.0.0.0:4500
client:
    typescript:
        path: override-2
    openapi:
        path: docs/static/include2.yml
        exclude_list: []
validators:
    - name: alice
      bonded: 100stake
    - name: alice
      bonded: 100000000stake
    - name: validator1
      bonded: 100000000stake`, server),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			baseReader := bytes.NewReader([]byte(tt.baseConfig))
			baseConfig, err := chainconfig.Parse(baseReader)
			if tt.err != nil {
				require.Error(t, err)
				require.Equal(t, tt.err.Error(), err.Error())
				return
			}
			require.NoError(t, err)

			finalConfigYaml, err := yaml.Marshal(baseConfig)
			require.NoError(t, err)
			require.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(string(finalConfigYaml)))
		})
	}
}

func startTestServer(t *testing.T) string {
	t.Helper()

	mux := http.NewServeMux()
	mux.HandleFunc("/include1.yml", func(w http.ResponseWriter, r *http.Request) {
		content, err := os.ReadFile("testdata/include1.yml")
		require.NoError(t, err)
		_, err = w.Write(content)
		require.NoError(t, err)
	})
	mux.HandleFunc("/include2.yml", func(w http.ResponseWriter, r *http.Request) {
		content, err := os.ReadFile("testdata/include2.yml")
		require.NoError(t, err)
		_, err = w.Write(content)
		require.NoError(t, err)
	})

	ports, err := availableport.Find(1)
	require.NoError(t, err)

	server := &http.Server{
		Addr:    fmt.Sprintf(":%d", ports[0]),
		Handler: mux,
	}

	listener, err := net.Listen("tcp", server.Addr)
	require.NoError(t, err)

	go server.Serve(listener)

	t.Cleanup(func() {
		server.Close()
	})

	return fmt.Sprintf("http://localhost:%d", ports[0])
}
