package cmd

import (
	"bytes"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"nest/src/catalog"
)

const testCatalogYAML = `
version: 1
tools:
  - name: nmap
    description: Network exploration tool and port scanner
    package: nmap
    executables:
      - /usr/bin/nmap
    images:
      - docker.io/parrotsec/security
    container:
      network: host
      cap_add:
        - NET_RAW
    tags:
      - recon
  - name: metasploit
    description: Penetration testing framework
    package: metasploit-framework
    executables:
      - /usr/bin/msfconsole
      - /usr/bin/msfvenom
    images:
      - docker.io/parrotsec/security
  - name: wireshark
    description: Network protocol analyzer
    package: wireshark
`

// testApp builds an App wired to buffers and temp paths, with a valid system
// catalog and no config files.
func testApp(t *testing.T, env map[string]string) (*App, *bytes.Buffer, *bytes.Buffer) {
	t.Helper()
	dir := t.TempDir()
	system := filepath.Join(dir, "system", "tools.yaml")
	if err := os.MkdirAll(filepath.Dir(system), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(system, []byte(testCatalogYAML), 0o644); err != nil {
		t.Fatal(err)
	}
	stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
	app := &App{
		Stdout:       stdout,
		Stderr:       stderr,
		Stdin:        strings.NewReader(""),
		StdinTTY:     false,
		Getenv:       func(k string) string { return env[k] },
		Version:      "test",
		CatalogLocs:  catalog.Locations{User: filepath.Join(dir, "user", "tools.yaml"), System: system},
		ConfigSystem: filepath.Join(dir, "etc", "nest.yaml"),
		ConfigUser:   filepath.Join(dir, "home", "nest.yaml"),
	}
	return app, stdout, stderr
}

// fakeRuntime installs a fake engine script on PATH and returns the path of
// the log file it appends every invocation to.
func fakeRuntime(t *testing.T, name, script string) string {
	t.Helper()
	dir := t.TempDir()
	log := filepath.Join(dir, "calls.log")
	if err := os.WriteFile(filepath.Join(dir, name), []byte(script), 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
	t.Setenv("FAKE_LOG", log)
	return log
}

func readLog(t *testing.T, log string) string {
	t.Helper()
	data, err := os.ReadFile(log)
	if err != nil {
		return ""
	}
	return string(data)
}

func TestNoArgsPrintsHelp(t *testing.T) {
	app, stdout, _ := testApp(t, nil)
	if code := app.Run(nil); code != exitOK {
		t.Errorf("exit = %d, want 0", code)
	}
	if !strings.Contains(stdout.String(), "Usage: nest <command>") {
		t.Error("no-command output should be the help text")
	}
}

func TestUnknownCommand(t *testing.T) {
	app, _, _ := testApp(t, nil)
	if code := app.Run([]string{"frobnicate"}); code != exitUsage {
		t.Errorf("exit = %d, want 2", code)
	}
}

func TestListAll(t *testing.T) {
	app, stdout, _ := testApp(t, nil)
	if code := app.Run([]string{"list"}); code != exitOK {
		t.Fatalf("exit = %d", code)
	}
	out := stdout.String()
	if !strings.Contains(out, "STATUS") || !strings.Contains(out, "CONTAINER") {
		t.Errorf("list must always show STATUS and CONTAINER columns:\n%s", out)
	}
	if !strings.Contains(out, "nmap") || !strings.Contains(out, "metasploit") || !strings.Contains(out, "wireshark") {
		t.Errorf("list --all output missing tools:\n%s", out)
	}
	// wireshark has no image; the others do
	lines := strings.Split(out, "\n")
	for _, line := range lines {
		if strings.HasPrefix(line, "wireshark") && !strings.Contains(line, "no") {
			t.Errorf("wireshark row should be marked container=no:\n%s", line)
		}
		if strings.HasPrefix(line, "nmap") && !strings.Contains(line, "yes") {
			t.Errorf("nmap row should be marked container=yes:\n%s", line)
		}
	}
}

func TestListAvailable(t *testing.T) {
	// §4.1: --available = not installed AND container available.
	app, stdout, _ := testApp(t, nil)
	if code := app.Run([]string{"list", "--available"}); code != exitOK {
		t.Fatalf("exit = %d", code)
	}
	out := stdout.String()
	if !strings.Contains(out, "nmap") {
		t.Errorf("nmap is a container candidate:\n%s", out)
	}
	if strings.Contains(out, "wireshark") {
		t.Errorf("wireshark has no container, must not be --available:\n%s", out)
	}
}

func TestListContainerModeDefaultsInstalled(t *testing.T) {
	app, stdout, _ := testApp(t, map[string]string{"container": "podman"})
	if code := app.Run([]string{"list"}); code != exitOK {
		t.Fatalf("exit = %d", code)
	}
	out := stdout.String()
	if strings.Contains(out, "nmap") || strings.Contains(out, "wireshark") {
		t.Errorf("no tools are installed in the test env, want headers only:\n%s", out)
	}
	if !strings.Contains(out, "STATUS") || !strings.Contains(out, "CONTAINER") {
		t.Errorf("headers must still show both status columns:\n%s", out)
	}
}

func TestListJSON(t *testing.T) {
	app, stdout, _ := testApp(t, nil)
	if code := app.Run([]string{"list", "--json"}); code != exitOK {
		t.Fatalf("exit = %d", code)
	}
	out := stdout.String()
	for _, key := range []string{`"name"`, `"description"`, `"package"`, `"installed"`, `"container_image"`, `"tags"`} {
		if !strings.Contains(out, key) {
			t.Errorf("JSON missing key %s:\n%s", key, out)
		}
	}
	if !strings.Contains(out, `"tags": [`) {
		t.Errorf("tags must serialize as an array (§4.8):\n%s", out)
	}
	if !strings.Contains(out, `"container_image": null`) {
		t.Errorf("imageless tools must have container_image: null:\n%s", out)
	}
}

func TestSearch(t *testing.T) {
	app, stdout, _ := testApp(t, nil)
	if code := app.Run([]string{"search", "recon"}); code != exitOK {
		t.Fatalf("exit = %d", code)
	}
	out := stdout.String()
	if !strings.Contains(out, "nmap") || strings.Contains(out, "metasploit") {
		t.Errorf("search recon should match only nmap:\n%s", out)
	}
}

func TestSearchNoMatch(t *testing.T) {
	app, stdout, _ := testApp(t, nil)
	if code := app.Run([]string{"search", "zzz"}); code != exitOK {
		t.Errorf("no match: exit = %d, want 0", code)
	}
	if strings.Contains(stdout.String(), "nmap") {
		t.Error("no match: output should be headers only")
	}
}

func TestInfoNotInstalled(t *testing.T) {
	app, stdout, _ := testApp(t, nil)
	if code := app.Run([]string{"info", "nmap"}); code != exitOK {
		t.Fatalf("exit = %d", code)
	}
	out := stdout.String()
	for _, want := range []string{
		"Status:      not installed",
		"/usr/bin/nmap (missing)",
		"network=host, cap_add=NET_RAW",
		"Run:         nest run nmap --container -- <args>",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("info output missing %q:\n%s", want, out)
		}
	}
}

func TestInfoNoContainer(t *testing.T) {
	app, stdout, _ := testApp(t, nil)
	if code := app.Run([]string{"info", "wireshark"}); code != exitOK {
		t.Fatalf("exit = %d", code)
	}
	out := stdout.String()
	for _, want := range []string{
		"Images:      (none)",
		"/usr/bin/wireshark (missing)", // executables default per §2.3
		"Run:         sudo apt install wireshark",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("info output missing %q:\n%s", want, out)
		}
	}
}

func TestInfoUnknownTool(t *testing.T) {
	app, _, _ := testApp(t, nil)
	if code := app.Run([]string{"info", "aircrack-ng"}); code != exitNotFound {
		t.Errorf("exit = %d, want 3", code)
	}
}

func TestVersion(t *testing.T) {
	app, stdout, _ := testApp(t, nil)
	if code := app.Run([]string{"version"}); code != exitOK {
		t.Fatalf("exit = %d", code)
	}
	out := stdout.String()
	if !strings.Contains(out, "nest version:    test") || !strings.Contains(out, "catalog version: 1") {
		t.Errorf("version output:\n%s", out)
	}
}

func TestVersionJSON(t *testing.T) {
	app, stdout, _ := testApp(t, nil)
	if code := app.Run([]string{"version", "--json"}); code != exitOK {
		t.Fatalf("exit = %d", code)
	}
	out := stdout.String()
	for _, key := range []string{`"nest": "test"`, `"catalog_version": 1`, `"catalog_source"`, `"runtime"`, `"container_mode"`} {
		if !strings.Contains(out, key) {
			t.Errorf("JSON missing %s:\n%s", key, out)
		}
	}
}

func TestMalformedConfigIsFatal(t *testing.T) {
	app, _, _ := testApp(t, nil)
	if err := os.MkdirAll(filepath.Dir(app.ConfigUser), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(app.ConfigUser, []byte("runtime: ["), 0o644); err != nil {
		t.Fatal(err)
	}
	if code := app.Run([]string{"list"}); code != exitError {
		t.Errorf("exit = %d, want 1 for malformed config", code)
	}
}
