package sbom_test

import (
	"testing"

	"github.com/forgeo/forgebucket/internal/domain/sbom"
)

// ─── go.mod ──────────────────────────────────────────────────────────────────

func TestParseGoMod_Block(t *testing.T) {
	content := []byte(`module github.com/example/app

go 1.21

require (
	github.com/go-chi/chi/v5 v5.2.5
	golang.org/x/crypto v0.50.0 // indirect
)

require github.com/lib/pq v1.12.3
`)
	comps := sbom.ParseGoMod(content)
	if len(comps) != 3 {
		t.Fatalf("expected 3 components, got %d", len(comps))
	}

	byName := make(map[string]sbom.Component)
	for _, c := range comps {
		byName[c.Name] = c
	}

	if c, ok := byName["github.com/go-chi/chi/v5"]; !ok {
		t.Error("missing github.com/go-chi/chi/v5")
	} else {
		if c.Version != "v5.2.5" {
			t.Errorf("wrong version: %s", c.Version)
		}
		if c.PURL != "pkg:golang/github.com/go-chi/chi/v5@v5.2.5" {
			t.Errorf("wrong PURL: %s", c.PURL)
		}
	}

	if _, ok := byName["golang.org/x/crypto"]; !ok {
		t.Error("missing golang.org/x/crypto (indirect deps must be included)")
	}
	if _, ok := byName["github.com/lib/pq"]; !ok {
		t.Error("missing github.com/lib/pq (single-line require)")
	}
}

func TestParseGoMod_Empty(t *testing.T) {
	comps := sbom.ParseGoMod([]byte("module foo\n\ngo 1.21\n"))
	if len(comps) != 0 {
		t.Errorf("expected 0 components, got %d", len(comps))
	}
}

// ─── package.json ─────────────────────────────────────────────────────────────

func TestParsePackageJSON(t *testing.T) {
	content := []byte(`{
		"name": "my-app",
		"dependencies": {
			"react": "^18.2.0",
			"axios": "1.4.0"
		},
		"devDependencies": {
			"vitest": "~1.0.0"
		}
	}`)

	comps := sbom.ParsePackageJSON(content)
	if len(comps) != 3 {
		t.Fatalf("expected 3 components, got %d", len(comps))
	}

	byName := make(map[string]sbom.Component)
	for _, c := range comps {
		byName[c.Name] = c
	}

	if c, ok := byName["react"]; !ok {
		t.Error("missing react")
	} else {
		if c.Version != "18.2.0" {
			t.Errorf("expected version stripped of ^, got %s", c.Version)
		}
		if c.PURL != "pkg:npm/react@18.2.0" {
			t.Errorf("wrong PURL: %s", c.PURL)
		}
		if c.Scope != "required" {
			t.Errorf("expected scope required, got %s", c.Scope)
		}
	}

	if c, ok := byName["vitest"]; !ok {
		t.Error("missing vitest")
	} else if c.Scope != "optional" {
		t.Errorf("devDependency should be optional, got %s", c.Scope)
	}
}

func TestParsePackageJSON_Invalid(t *testing.T) {
	comps := sbom.ParsePackageJSON([]byte("not json"))
	if len(comps) != 0 {
		t.Errorf("expected 0 on invalid JSON, got %d", len(comps))
	}
}

// ─── requirements.txt ────────────────────────────────────────────────────────

func TestParseRequirementsTxt(t *testing.T) {
	content := []byte(`# comment
requests==2.31.0
flask>=2.3.0
numpy~=1.24.0
boto3[s3]==1.28.0  # with extras
no-version-package
-r other-requirements.txt
`)
	comps := sbom.ParseRequirementsTxt(content)

	byName := make(map[string]sbom.Component)
	for _, c := range comps {
		byName[c.Name] = c
	}

	if c, ok := byName["requests"]; !ok {
		t.Error("missing requests")
	} else if c.Version != "2.31.0" {
		t.Errorf("requests version: %s", c.Version)
	}

	if c, ok := byName["flask"]; !ok {
		t.Error("missing flask")
	} else if c.Version != "2.3.0" {
		t.Errorf("flask version: %s", c.Version)
	}

	if _, ok := byName["boto3"]; !ok {
		t.Error("missing boto3 (extras should be stripped from name)")
	}

	if _, ok := byName["no-version-package"]; !ok {
		t.Error("missing no-version-package")
	}
}

// ─── Cargo.toml ──────────────────────────────────────────────────────────────

func TestParseCargoToml(t *testing.T) {
	content := []byte(`[package]
name = "my-crate"
version = "0.1.0"

[dependencies]
serde = "1.0"
tokio = { version = "1.28", features = ["full"] }
clap = "4.3"

[dev-dependencies]
criterion = "0.5"
`)
	comps := sbom.ParseCargoToml(content)
	if len(comps) != 4 {
		t.Fatalf("expected 4 components, got %d: %v", len(comps), comps)
	}

	byName := make(map[string]sbom.Component)
	for _, c := range comps {
		byName[c.Name] = c
	}

	if c, ok := byName["serde"]; !ok {
		t.Error("missing serde")
	} else if c.Version != "1.0" {
		t.Errorf("serde version: %s", c.Version)
	}

	if c, ok := byName["tokio"]; !ok {
		t.Error("missing tokio")
	} else if c.Version != "1.28" {
		t.Errorf("tokio version: %s", c.Version)
	}

	if _, ok := byName["criterion"]; !ok {
		t.Error("missing criterion (dev-dependency)")
	}
}

// ─── Gemfile.lock ─────────────────────────────────────────────────────────────

func TestParseGemfileLock(t *testing.T) {
	content := []byte(`GEM
  remote: https://rubygems.org/
  specs:
    rails (7.1.0)
      actionpack (= 7.1.0)
      railties (= 7.1.0)
    actionpack (7.1.0)
    rake (13.0.6)

PLATFORMS
  ruby

DEPENDENCIES
  rails (~> 7.1.0)
`)
	comps := sbom.ParseGemfileLock(content)
	if len(comps) != 3 {
		t.Fatalf("expected 3 components, got %d: %v", len(comps), comps)
	}

	byName := make(map[string]sbom.Component)
	for _, c := range comps {
		byName[c.Name] = c
	}

	if c, ok := byName["rails"]; !ok {
		t.Error("missing rails")
	} else if c.Version != "7.1.0" {
		t.Errorf("rails version: %s", c.Version)
	}

	if c, ok := byName["rake"]; !ok {
		t.Error("missing rake")
	} else if c.Version != "13.0.6" {
		t.Errorf("rake version: %s", c.Version)
	}
}

// ─── pom.xml ─────────────────────────────────────────────────────────────────

func TestParsePomXML(t *testing.T) {
	content := []byte(`<?xml version="1.0" encoding="UTF-8"?>
<project>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>3.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <version>42.6.0</version>
    </dependency>
  </dependencies>
</project>`)

	comps := sbom.ParsePomXML(content)
	if len(comps) != 2 {
		t.Fatalf("expected 2 components, got %d", len(comps))
	}

	byName := make(map[string]sbom.Component)
	for _, c := range comps {
		byName[c.Name] = c
	}

	if c, ok := byName["org.springframework.boot:spring-boot-starter-web"]; !ok {
		t.Error("missing spring-boot-starter-web")
	} else {
		if c.Version != "3.1.0" {
			t.Errorf("spring-boot version: %s", c.Version)
		}
		if c.PURL != "pkg:maven/org.springframework.boot/spring-boot-starter-web@3.1.0" {
			t.Errorf("wrong PURL: %s", c.PURL)
		}
	}
}

// ─── Document builder ─────────────────────────────────────────────────────────

func TestNewDocument(t *testing.T) {
	doc := sbom.NewDocument("my-repo", "abc1234567890")
	if doc.BOMFormat != "CycloneDX" {
		t.Errorf("BOMFormat: %s", doc.BOMFormat)
	}
	if doc.SpecVersion != "1.4" {
		t.Errorf("SpecVersion: %s", doc.SpecVersion)
	}
	if doc.Metadata.Component.Name != "my-repo" {
		t.Errorf("metadata component name: %s", doc.Metadata.Component.Name)
	}
	if len(doc.Metadata.Tools) == 0 {
		t.Error("expected at least one tool in metadata")
	}
	if doc.Components == nil {
		t.Error("expected non-nil Components slice")
	}
}
