package hnblogs

import (
	"bytes"
	"encoding/json"
	"os/exec"
	"strings"
	"testing"
)

func TestGetBlogs(t *testing.T) {
	blogs, err := GetBlogs()
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if len(blogs) == 0 {
		t.Fatalf("Expected the embedded dataset to be non-empty, got %d blogs", len(blogs))
	}
}

// TestEmbeddedDataIsUsable guards the invariant the embedded dataset has to
// satisfy for the rest of the package: every entry has a URL, so callers of
// RandomBlog and NthBlog get something dereferenceable.
func TestEmbeddedDataIsUsable(t *testing.T) {
	if !json.Valid(blogsJSON) {
		t.Fatalf("Embedded blogs.json is not valid JSON")
	}

	blogs, err := GetBlogs()
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	for i, blog := range blogs {
		if blog.URL == "" {
			t.Fatalf("Blog at index %d has an empty URL", i)
		}
	}
}

// TestNoRuntimeNetworkImports is the regression guard for the reason this data
// is embedded: the package must not reach the network on any code path. It
// walks the whole dependency graph of the non-test build rather than the direct
// import list, so a net/* package reached through an intermediate import fails
// here too. Test-only imports are outside that graph, which is why this test
// may use os/exec itself.
func TestNoRuntimeNetworkImports(t *testing.T) {
	cmd := exec.Command("go", "list", "-deps", ".")

	var stderr bytes.Buffer
	cmd.Stderr = &stderr

	out, err := cmd.Output()
	if err != nil {
		t.Fatalf("Failed to list package dependencies: %v\n%s", err, stderr.String())
	}

	for _, dep := range strings.Fields(string(out)) {
		if dep == "net" || strings.HasPrefix(dep, "net/") {
			t.Fatalf("Package must not perform network I/O, but depends on %q", dep)
		}
	}
}

func TestGetBlogsIsMemoized(t *testing.T) {
	first, err := GetBlogs()
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	second, err := GetBlogs()
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if len(first) != len(second) {
		t.Fatalf("Expected the same slice on repeated calls, got %d then %d", len(first), len(second))
	}

	if len(first) > 0 && &first[0] != &second[0] {
		t.Fatalf("Expected repeated calls to share the memoized backing array")
	}
}

func TestRandomBlog(t *testing.T) {
	blog, err := RandomBlog()
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if blog.URL == "" {
		t.Fatalf("Expected a valid blog, got an empty URL")
	}
}

func TestRandomBlogs(t *testing.T) {
	n := 5
	randomBlogs, err := RandomBlogs(n)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if len(randomBlogs) != n {
		t.Fatalf("Expected %d random blogs, got %d", n, len(randomBlogs))
	}

	for _, blog := range randomBlogs {
		if blog.URL == "" {
			t.Fatalf("Expected a valid blog, got an empty URL")
		}
	}
}

func TestRandomBlogsRejectsInvalidCounts(t *testing.T) {
	blogs, err := GetBlogs()
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	for _, n := range []int{0, -1, len(blogs) + 1} {
		if _, err := RandomBlogs(n); err == nil {
			t.Fatalf("Expected an error for n=%d, got none", n)
		}
	}
}

func TestNthBlog(t *testing.T) {
	blogs, err := GetBlogs()
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	blog, err := NthBlog(5)
	if err != nil {
		t.Fatalf("Expected no error, got %v", err)
	}

	if blog.URL != blogs[5].URL {
		t.Fatalf("Expected %s, got %s", blogs[5].URL, blog.URL)
	}

	_, err = NthBlog(len(blogs))
	if err == nil {
		t.Fatalf("Expected error for out-of-range index, got none")
	}

	if _, err := NthBlog(-1); err == nil {
		t.Fatalf("Expected error for negative index, got none")
	}
}
