// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-present Datadog, Inc.

package tests

import (
	"bufio"
	"encoding/csv"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
)

func skipUnlessCompliance(t *testing.T) {
	t.Helper()
	if os.Getenv("RSHELL_COMPLIANCE_TEST") == "" {
		t.Skip("skipping compliance tests (set RSHELL_COMPLIANCE_TEST=1 to enable)")
	}
}

// repoRoot returns the absolute path to the repository root (parent of tests/).
func repoRoot(t *testing.T) string {
	t.Helper()
	// This file lives in tests/, so the repo root is one level up.
	dir, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	root := filepath.Dir(dir)
	// Sanity check: go.mod should exist at the root.
	if _, err := os.Stat(filepath.Join(root, "go.mod")); err != nil {
		t.Fatalf("could not locate repo root (expected go.mod at %s): %v", root, err)
	}
	return root
}

func TestComplianceRequiredFiles(t *testing.T) {
	skipUnlessCompliance(t)
	root := repoRoot(t)
	required := []string{
		"LICENSE",
		"NOTICE",
		"LICENSE-3rdparty.csv",
		"README.md",
		"CONTRIBUTING.md",
		".github/PULL_REQUEST_TEMPLATE.md",
		".github/ISSUE_TEMPLATE/bug_report.md",
		".github/ISSUE_TEMPLATE/feature_request.md",
	}
	for _, f := range required {
		path := filepath.Join(root, filepath.FromSlash(f))
		if _, err := os.Stat(path); err != nil {
			t.Errorf("required file missing: %s", f)
		}
	}
}

var copyrightLineRe = regexp.MustCompile(`^// Copyright 20\d{2}-present Datadog, Inc\.$`)

func TestComplianceCopyrightHeaders(t *testing.T) {
	skipUnlessCompliance(t)
	root := repoRoot(t)
	expectedLines := []string{
		"// Unless explicitly stated otherwise all files in this repository are licensed",
		"// under the Apache License Version 2.0.",
		"// This product includes software developed at Datadog (https://www.datadoghq.com/).",
	}

	err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			if d.Name() == "vendor" {
				return fs.SkipDir
			}
			return nil
		}
		if !strings.HasSuffix(d.Name(), ".go") {
			return nil
		}

		rel, _ := filepath.Rel(root, path)
		f, err := os.Open(path)
		if err != nil {
			t.Errorf("%s: %v", rel, err)
			return nil
		}
		defer f.Close()

		// Collect the first 4 non-empty lines.
		var lines []string
		scanner := bufio.NewScanner(f)
		for scanner.Scan() && len(lines) < 4 {
			line := scanner.Text()
			if strings.TrimSpace(line) != "" {
				lines = append(lines, line)
			}
		}

		if len(lines) < 4 {
			t.Errorf("%s: not enough lines for copyright header", rel)
			return nil
		}
		for i, exp := range expectedLines {
			if lines[i] != exp {
				t.Errorf("%s: header line %d mismatch\n  got:  %s\n  want: %s", rel, i+1, lines[i], exp)
				return nil
			}
		}
		if !copyrightLineRe.MatchString(lines[3]) {
			t.Errorf("%s: header line 4 mismatch\n  got:  %s\n  want: // Copyright 20XX-present Datadog, Inc.", rel, lines[3])
		}
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}
}

func TestComplianceLicense3rdPartyFormat(t *testing.T) {
	skipUnlessCompliance(t)
	root := repoRoot(t)
	f, err := os.Open(filepath.Join(root, "LICENSE-3rdparty.csv"))
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()

	reader := csv.NewReader(f)
	records, err := reader.ReadAll()
	if err != nil {
		t.Fatalf("failed to parse LICENSE-3rdparty.csv: %v", err)
	}
	if len(records) == 0 {
		t.Fatal("LICENSE-3rdparty.csv is empty")
	}

	// Check header.
	expectedHeader := []string{"Component", "Origin", "License", "Copyright"}
	header := records[0]
	if len(header) != len(expectedHeader) {
		t.Fatalf("header field count: got %d, want %d", len(header), len(expectedHeader))
	}
	for i, h := range expectedHeader {
		if header[i] != h {
			t.Errorf("header field %d: got %q, want %q", i, header[i], h)
		}
	}

	// Known SPDX identifiers (extend as needed).
	knownSPDX := map[string]bool{
		"MIT":                true,
		"Apache-2.0":         true,
		"BSD-2-Clause":       true,
		"BSD-3-Clause":       true,
		"ISC":                true,
		"MPL-2.0":            true,
		"MIT AND Apache-2.0": true,
	}

	for i, row := range records[1:] {
		lineNum := i + 2 // 1-indexed, skip header
		if len(row) != 4 {
			t.Errorf("line %d: got %d fields, want 4", lineNum, len(row))
			continue
		}
		license := row[2]
		if !knownSPDX[license] {
			t.Errorf("line %d: unknown SPDX license identifier: %q", lineNum, license)
		}
	}
}

func TestComplianceLicense3rdPartyCompleteness(t *testing.T) {
	skipUnlessCompliance(t)
	root := repoRoot(t)

	// Parse modules from go.mod.
	gomod, err := os.ReadFile(filepath.Join(root, "go.mod"))
	if err != nil {
		t.Fatal(err)
	}
	modules := parseGoModRequires(string(gomod))

	// Parse components from LICENSE-3rdparty.csv.
	csvData, err := os.ReadFile(filepath.Join(root, "LICENSE-3rdparty.csv"))
	if err != nil {
		t.Fatal(err)
	}
	components := parseLicense3rdPartyComponents(string(csvData))

	for _, mod := range modules {
		if !components[mod] {
			t.Errorf("module %s is in go.mod but missing from LICENSE-3rdparty.csv", mod)
		}
	}
}

// parseGoModRequires extracts module paths from require blocks in go.mod.
func parseGoModRequires(content string) []string {
	var modules []string
	inRequire := false
	for _, line := range strings.Split(content, "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "require (") {
			inRequire = true
			continue
		}
		if line == ")" {
			inRequire = false
			continue
		}
		if inRequire && line != "" {
			parts := strings.Fields(line)
			if len(parts) >= 1 {
				modules = append(modules, parts[0])
			}
		}
	}
	return modules
}

// parseLicense3rdPartyComponents returns a set of component names from the CSV.
func parseLicense3rdPartyComponents(content string) map[string]bool {
	components := make(map[string]bool)
	for i, line := range strings.Split(content, "\n") {
		if i == 0 || strings.TrimSpace(line) == "" {
			continue // skip header and blank lines
		}
		if idx := strings.IndexByte(line, ','); idx > 0 {
			components[line[:idx]] = true
		}
	}
	return components
}
