// Copyright 2020 Nokia
// Licensed under the BSD 3-Clause License.
// SPDX-License-Identifier: BSD-3-Clause

package core

import (
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"gopkg.in/yaml.v2"
)

func TestGenerateAnsibleInventoryCredentialPlacement(t *testing.T) {
	t.Run("defaults_in_all_vars", func(t *testing.T) {
		c, err := NewContainerLab(WithTopoPath("test_data/topo_inv_defaults_creds.yml", nil))
		if err != nil {
			t.Fatal(err)
		}
		var b strings.Builder
		if err := c.generateAnsibleInventory(&b); err != nil {
			t.Fatal(err)
		}
		got := b.String()
		wantFrag := "ansible_httpapi_use_proxy: false\n    ansible_user: labuser\n    ansible_password: labpass"
		if !strings.Contains(got, wantFrag) {
			t.Fatalf("expected defaults credentials in all.vars, missing fragment; got:\n%s", got)
		}
		if n := strings.Count(got, "ansible_user: labuser"); n != 1 {
			t.Fatalf(
				"expected exactly one ansible_user: labuser (all.vars), got count=%d:\n%s",
				n,
				got,
			)
		}
	})

	t.Run("kind_in_group_vars", func(t *testing.T) {
		c, err := NewContainerLab(WithTopoPath("test_data/topo_inv_kind_creds.yml", nil))
		if err != nil {
			t.Fatal(err)
		}
		var b strings.Builder
		if err := c.generateAnsibleInventory(&b); err != nil {
			t.Fatal(err)
		}
		got := b.String()
		head, _, ok := strings.Cut(got, "  children:")
		if !ok {
			t.Fatal("missing children in inventory")
		}
		if strings.Contains(head, "ansible_user:") ||
			strings.Contains(head, "ansible_password:") {
			t.Fatalf("did not expect credentials in all.vars head, got head:\n%s", head)
		}
		if !strings.Contains(got, "ansible_user: kinduser") ||
			!strings.Contains(got, "ansible_password: kindpass") {
			t.Fatalf("expected kind credentials in inventory, got:\n%s", got)
		}
		if n := strings.Count(got, "ansible_user: kinduser"); n != 1 {
			t.Fatalf("expected kind-level ansible_user only once in vars, got count=%d", n)
		}
	})

	t.Run("heterogeneous_node_passwords_per_host", func(t *testing.T) {
		c, err := NewContainerLab(WithTopoPath("test_data/topo_inv_two_node_passwords.yml", nil))
		if err != nil {
			t.Fatal(err)
		}
		var b strings.Builder
		if err := c.generateAnsibleInventory(&b); err != nil {
			t.Fatal(err)
		}
		got := b.String()
		if !strings.Contains(got, "ansible_password: secret-one") ||
			!strings.Contains(got, "ansible_password: secret-two") {
			t.Fatalf("expected per-host passwords, got:\n%s", got)
		}
		if strings.Contains(got, "      vars:\n        ansible_network_os:") &&
			strings.Contains(got[strings.Index(got, "      vars:"):], "ansible_password: secret") {
			// vars block after first "      vars:" might still include connection-only; ensure no
			// kind-level password line
			idx := strings.Index(got, "ansible_network_os:")
			if idx < 0 {
				t.Fatal("missing network_os")
			}
			rest := got[idx:]
			endHosts := strings.Index(rest, "      hosts:")
			if endHosts < 0 {
				t.Fatal("missing hosts")
			}
			kindVars := rest[:endHosts]
			if strings.Contains(kindVars, "ansible_password:") {
				t.Fatalf(
					"did not expect ansible_password in kind vars, got kind vars section:\n%s",
					kindVars,
				)
			}
		}
	})
}

func TestGenerateNornirInventoryCredentialPlacement(t *testing.T) {
	c, err := NewContainerLab(WithTopoPath("test_data/topo_inv_two_node_passwords.yml", nil))
	if err != nil {
		t.Fatal(err)
	}
	var b strings.Builder
	if err := c.generateNornirSimpleInventory(&b); err != nil {
		t.Fatal(err)
	}
	var got map[string]any
	if err := yaml.Unmarshal([]byte(b.String()), &got); err != nil {
		t.Fatalf("yaml: %v", err)
	}
	n1 := got["n1"].(map[interface{}]interface{})
	n2 := got["n2"].(map[interface{}]interface{})
	if n1["password"] != "secret-one" || n2["password"] != "secret-two" {
		t.Fatalf("expected per-host passwords, n1=%v n2=%v", n1["password"], n2["password"])
	}
}

func TestGenerateAnsibleInventory(t *testing.T) {
	tests := map[string]struct {
		got  string
		want string
	}{
		"case1": {
			got: "test_data/topo1.yml",
			want: `all:
  vars:
    # The generated inventory is assumed to be used from the clab host.
    # Hence no http proxy should be used. Therefore we make sure the http
    # module does not attempt using any global http proxy.
    ansible_httpapi_use_proxy: false
  children:
    nokia_srlinux:
      vars:
        ansible_network_os: nokia.srlinux.srlinux
        # default connection type for nodes of this kind
        # feel free to override this in your inventory
        ansible_connection: ansible.netcommon.httpapi
        ansible_user: admin
        ansible_password: NokiaSrl1!
      hosts:
        clab-topo1-node1:
          ansible_host: 172.100.100.11
        clab-topo1-node2:
          ansible_host: 172.100.100.12
`,
		},
		"case2": {
			got: "test_data/topo8_ansible_groups.yml",
			want: `all:
  vars:
    # The generated inventory is assumed to be used from the clab host.
    # Hence no http proxy should be used. Therefore we make sure the http
    # module does not attempt using any global http proxy.
    ansible_httpapi_use_proxy: false
  children:
    linux:
      hosts:
        clab-topo8_ansible_groups-node4:
    nokia_srlinux:
      vars:
        ansible_network_os: nokia.srlinux.srlinux
        # default connection type for nodes of this kind
        # feel free to override this in your inventory
        ansible_connection: ansible.netcommon.httpapi
        ansible_user: admin
        ansible_password: NokiaSrl1!
      hosts:
        clab-topo8_ansible_groups-node1:
          ansible_host: 172.100.100.11
        clab-topo8_ansible_groups-node2:
          ansible_host: 172.100.100.12
        clab-topo8_ansible_groups-node3:
          ansible_host: 172.100.100.13
    extra_group:
      hosts:
        clab-topo8_ansible_groups-node2:
          ansible_host: 172.100.100.12
        clab-topo8_ansible_groups-node3:
          ansible_host: 172.100.100.13
    spine:
      hosts:
        clab-topo8_ansible_groups-node1:
          ansible_host: 172.100.100.11
`,
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			opts := []ClabOption{
				WithTopoPath(tc.got, nil),
			}

			c, err := NewContainerLab(opts...)
			if err != nil {
				t.Fatal(err)
			}

			var s strings.Builder

			err = c.generateAnsibleInventory(&s)
			if err != nil {
				t.Fatal(err)
			}

			if diff := cmp.Diff(tc.want, s.String()); diff != "" {
				t.Errorf("failed at '%s', diff: (-want +got)\n%s", name, diff)
			}
		})
	}
}

func TestGeneratNornirSimpleInventory(t *testing.T) {
	tests := map[string]struct {
		got                              string
		want                             string
		clab_nornir_platform_name_schema string // environment variable feature flag to direct platform name
	}{
		"case1-default-platform": {
			got:                              "test_data/topo1.yml",
			clab_nornir_platform_name_schema: "",
			want: `---
node1:
    username: admin
    password: NokiaSrl1!
    platform: nokia_srlinux
    hostname: 172.100.100.11
node2:
    username: admin
    password: NokiaSrl1!
    platform: nokia_srlinux
    hostname: 172.100.100.12`,
		},
		"case2-scrapli-platform": {
			got:                              "test_data/topo12.yml",
			clab_nornir_platform_name_schema: "scrapli",
			want: `---
node1:
    username: admin
    password: admin
    platform: arista_eos
    hostname:
node2:
    username: admin
    password: admin
    platform: arista_eos
    hostname:
node3:
    username: admin
    password: admin
    platform: arista_eos
    hostname:
node4:
    username:
    password:
    platform: linux
    hostname: `,
		},
		"case3-nornir-platform": {
			got:                              "test_data/topo12.yml",
			clab_nornir_platform_name_schema: "napalm",
			want: `---
node1:
    username: admin
    password: admin
    platform: eos
    hostname:
node2:
    username: admin
    password: admin
    platform: eos
    hostname:
node3:
    username: admin
    password: admin
    platform: eos
    hostname:
node4:
    username:
    password:
    platform: linux
    hostname: `,
		},
		"case4-default-groups-platform": {
			got:                              "test_data/topo8_nornir_groups.yml",
			clab_nornir_platform_name_schema: "",
			want: `---
node4:
    username:
    password:
    platform: linux
    hostname: 172.100.100.14
node1:
    username: admin
    password: NokiaSrl1!
    platform: nokia_srlinux
    hostname: 172.100.100.11
    groups:
      - spine
node2:
    username: admin
    password: NokiaSrl1!
    platform: nokia_srlinux
    hostname: 172.100.100.12
    groups:
      - extra_group
      - second_extra_group
node3:
    username: admin
    password: NokiaSrl1!
    platform: nokia_srlinux
    hostname: 172.100.100.13
    groups:
      - extra_group`,
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			// Set the environment variable
			t.Setenv("CLAB_NORNIR_PLATFORM_NAME_SCHEMA", tc.clab_nornir_platform_name_schema)

			opts := []ClabOption{
				WithTopoPath(tc.got, nil),
			}

			c, err := NewContainerLab(opts...)
			if err != nil {
				t.Fatal(err)
			}

			var s strings.Builder

			err = c.generateNornirSimpleInventory(&s)
			if err != nil {
				t.Fatal(err)
			}

			// compare "real" objects so we dont have to faff w/ whitespace or having editors
			// add/remove trailing whitespace in the want strings in the test table
			var gotData, wantData map[string]any

			err = yaml.Unmarshal([]byte(s.String()), &gotData)
			if err != nil {
				t.Fatalf("failed to unmarshal got YAML: %v", err)
			}

			err = yaml.Unmarshal([]byte(tc.want), &wantData)
			if err != nil {
				t.Fatalf("failed to unmarshal want YAML: %v", err)
			}

			if diff := cmp.Diff(wantData, gotData); diff != "" {
				t.Errorf("failed at '%s', diff: (-want +got)\n%s", name, diff)
			}
		})
	}
}
