package integration

import (
	"cmp"
	"encoding/json"
	"fmt"
	"testing"

	clientv1 "github.com/juanfont/headscale/gen/client/v1"
	"github.com/juanfont/headscale/integration/hsic"
	"github.com/juanfont/headscale/integration/tsic"
	"github.com/stretchr/testify/require"
)

// This file holds the shared helpers used by the per-command CLI integration
// test files (cli_users_test.go, cli_nodes_test.go, cli_apikeys_test.go,
// cli_preauthkeys_test.go, cli_auth_test.go, cli_server_test.go and
// cli_policy_test.go). The tests themselves live in those files, grouped by
// the command they exercise.
//
// The whole point of the CLI test suite is to guard the transport: every
// command is invoked with `--output json` and the result is unmarshalled into
// the matching HTTP-client Go type, so a change to the API handlers,
// schema definitions or output encoders that breaks a command is caught here.

func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
	str, err := headscale.Execute(command)
	if err != nil {
		return err
	}

	err = json.Unmarshal([]byte(str), result)
	if err != nil {
		return fmt.Errorf("failed to unmarshal: %w\n command err: %s", err, str)
	}

	return nil
}

// assertJSONRoundtrip executes command (which must include `--output json`),
// decodes the stdout into T, then marshals T back to JSON and re-decodes it,
// asserting the serialisation is stable. This is the transport contract guard:
// if the underlying type drifts in a way that loses data, the round-trip
// breaks. The decoded value is returned so callers can assert on real fields.
func assertJSONRoundtrip[T any](t require.TestingT, headscale ControlServer, command []string) T {
	var first T

	err := executeAndUnmarshal(headscale, command, &first)
	require.NoError(t, err, "decoding CLI json output")

	firstBytes, err := json.Marshal(first)
	require.NoError(t, err, "re-marshalling decoded value")

	var second T

	require.NoError(t, json.Unmarshal(firstBytes, &second), "re-decoding marshalled value")

	secondBytes, err := json.Marshal(second)
	require.NoError(t, err, "re-marshalling round-tripped value")

	require.JSONEq(t, string(firstBytes), string(secondBytes), "json round-trip should be stable")

	return second
}

// sortWithID orders users by their numeric ID. The HTTP client emits IDs as
// decimal strings, so they are parsed back to integers to preserve numeric
// (not lexicographic) ordering.
func sortWithID(a, b *clientv1.User) int {
	return cmp.Compare(mustParseID(a.Id), mustParseID(b.Id))
}

// setupCLIScenario boots a scenario with the given users and nodes-per-user,
// creates the headscale environment and returns the running scenario and its
// control server. It removes the repeated NewScenario/CreateHeadscaleEnv/
// Headscale boilerplate shared by the CLI tests. Callers still defer
// scenario.ShutdownAssertNoPanics(t) themselves so the cleanup is visible at
// the call site.
func setupCLIScenario(t *testing.T, testName string, users []string, nodesPerUser int) (*Scenario, ControlServer) {
	t.Helper()

	spec := ScenarioSpec{
		Users:        users,
		NodesPerUser: nodesPerUser,
	}

	scenario, err := NewScenario(spec)
	require.NoError(t, err)

	err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName(testName))
	require.NoError(t, err)

	headscale, err := scenario.Headscale()
	require.NoError(t, err)

	return scenario, headscale
}
