package catalog

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

const validYAML = `
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
`

func TestParseValid(t *testing.T) {
	c, err := Parse([]byte(validYAML))
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	if c.Version != 1 {
		t.Errorf("version = %d, want 1", c.Version)
	}
	if len(c.Tools) != 2 {
		t.Fatalf("tools = %d, want 2", len(c.Tools))
	}
	nmap := c.Tools[0]
	if nmap.Container == nil || nmap.Container.Network != "host" {
		t.Errorf("container options not parsed: %+v", nmap.Container)
	}
	// nil collections normalize to empty so JSON never emits null (§4.8)
	if nmap.Container.ExtraArgs == nil {
		t.Error("ExtraArgs is nil, want empty slice")
	}
	if c.Tools[1].Tags == nil {
		t.Error("Tags is nil, want empty slice")
	}
	if c.Tools[1].Container != nil {
		t.Error("Container should stay nil when undeclared")
	}
}

func TestParseInvalid(t *testing.T) {
	cases := map[string]string{
		"malformed YAML":      "version: [",
		"unsupported version": "version: 2\ntools: []",
		"missing name":        "version: 1\ntools:\n  - description: d\n    package: p\n    executables: [/usr/bin/x]\n    images: [img]",
		"duplicate name": "version: 1\ntools:\n" +
			"  - {name: x, description: d, package: p, executables: [/usr/bin/x], images: [img]}\n" +
			"  - {name: x, description: d, package: p, executables: [/usr/bin/y], images: [img]}",
		"missing description": "version: 1\ntools:\n  - {name: x, package: p, executables: [/usr/bin/x], images: [img]}",
		"missing package":     "version: 1\ntools:\n  - {name: x, description: d, executables: [/usr/bin/x], images: [img]}",
		"relative executable": "version: 1\ntools:\n  - {name: x, description: d, package: p, executables: [usr/bin/x], images: [img]}",
	}
	for name, doc := range cases {
		t.Run(name, func(t *testing.T) {
			if _, err := Parse([]byte(doc)); err == nil {
				t.Fatal("Parse succeeded, want validation error")
			}
		})
	}
}

func TestDefaultsApplied(t *testing.T) {
	// §2.3: executables defaults to /usr/bin/<name>; images may be omitted
	// entirely, meaning the tool has no container available.
	c, err := Parse([]byte("version: 1\ntools:\n  - {name: wireshark, description: d, package: wireshark}"))
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	tool := c.Tools[0]
	if len(tool.Executables) != 1 || tool.Executables[0] != "/usr/bin/wireshark" {
		t.Errorf("Executables = %v, want [/usr/bin/wireshark]", tool.Executables)
	}
	if tool.Images == nil {
		t.Error("Images is nil, want empty slice (JSON key-presence rule)")
	}
	if tool.HasImage() {
		t.Error("HasImage = true, want false for imageless tool")
	}
}

func writeCatalog(t *testing.T, path, doc string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, []byte(doc), 0o644); err != nil {
		t.Fatal(err)
	}
}

func testLocations(t *testing.T) Locations {
	dir := t.TempDir()
	return Locations{
		User:   filepath.Join(dir, "user", "tools.yaml"),
		System: filepath.Join(dir, "system", "tools.yaml"),
	}
}

func TestResolveRejectsUnsupportedUserVersion(t *testing.T) {
	// Only version 1 is supported (§2.3); a newer user copy is invalid and
	// ignored with a warning, falling back to the system copy.
	loc := testLocations(t)
	writeCatalog(t, loc.System, validYAML)
	writeCatalog(t, loc.User, strings.Replace(validYAML, "version: 1", "version: 2", 1))

	var stderr bytes.Buffer
	c, src, err := Resolve("", loc, &stderr)
	if err != nil {
		t.Fatalf("Resolve: %v", err)
	}
	if c.Version != 1 || src != loc.System {
		t.Errorf("got version %d from %s, want 1 from system copy", c.Version, src)
	}
	if !strings.Contains(stderr.String(), "warning") {
		t.Error("expected a warning on stderr for the unsupported user copy")
	}
}

func TestResolveUserWinsTies(t *testing.T) {
	loc := testLocations(t)
	writeCatalog(t, loc.System, validYAML)
	writeCatalog(t, loc.User, validYAML)

	_, src, err := Resolve("", loc, &bytes.Buffer{})
	if err != nil {
		t.Fatalf("Resolve: %v", err)
	}
	if src != loc.User {
		t.Errorf("source = %s, want user copy on tie", src)
	}
}

func TestResolveMalformedUserFallsBack(t *testing.T) {
	loc := testLocations(t)
	writeCatalog(t, loc.System, validYAML)
	writeCatalog(t, loc.User, "version: [")

	var stderr bytes.Buffer
	c, src, err := Resolve("", loc, &stderr)
	if err != nil {
		t.Fatalf("Resolve: %v", err)
	}
	if src != loc.System {
		t.Errorf("source = %s, want system copy", src)
	}
	if c.Version != 1 {
		t.Errorf("version = %d, want 1", c.Version)
	}
	if !strings.Contains(stderr.String(), "warning") {
		t.Error("expected a warning on stderr for the malformed user copy")
	}
}

func TestResolveMalformedSystemIsFatal(t *testing.T) {
	loc := testLocations(t)
	writeCatalog(t, loc.System, "version: [")
	writeCatalog(t, loc.User, validYAML)

	if _, _, err := Resolve("", loc, &bytes.Buffer{}); err == nil {
		t.Fatal("Resolve succeeded, want fatal error for malformed system copy")
	}
}

func TestResolveNoCatalogIsFatal(t *testing.T) {
	loc := testLocations(t)
	if _, _, err := Resolve("", loc, &bytes.Buffer{}); err == nil {
		t.Fatal("Resolve succeeded, want error when no catalog exists")
	}
}

func TestResolveOverride(t *testing.T) {
	loc := testLocations(t)
	override := filepath.Join(t.TempDir(), "dev.yaml")
	writeCatalog(t, override, validYAML)

	_, src, err := Resolve(override, loc, &bytes.Buffer{})
	if err != nil {
		t.Fatalf("Resolve: %v", err)
	}
	if src != override {
		t.Errorf("source = %s, want override %s", src, override)
	}
}

func TestSearch(t *testing.T) {
	c, err := Parse([]byte(validYAML))
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	cases := []struct {
		term string
		want int
	}{
		{"nmap", 1},
		{"NMAP", 1},      // case-insensitive
		{"network", 1},   // description
		{"framework", 1}, // description of metasploit
		{"recon", 1},     // tag
		{"parrotsec", 0}, // images are not searched
		{"", 2},          // empty term matches everything
		{"nonexistent", 0},
	}
	for _, tc := range cases {
		if got := len(c.Search(tc.term)); got != tc.want {
			t.Errorf("Search(%q) = %d results, want %d", tc.term, got, tc.want)
		}
	}
}

func TestFind(t *testing.T) {
	c, err := Parse([]byte(validYAML))
	if err != nil {
		t.Fatalf("Parse: %v", err)
	}
	if _, ok := c.Find("nmap"); !ok {
		t.Error("Find(nmap) not found")
	}
	if _, ok := c.Find("wireshark"); ok {
		t.Error("Find(wireshark) found, want not found")
	}
}
