package apps

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

func TestKeyForURL(t *testing.T) {
	cases := []struct {
		raw, want string
	}{
		// Default ports are PRESERVED — no port-stripping (the previous
		// behavior conflated "operator wrote :443" with "operator wrote
		// bare host"; with the full origin in the key, every URL maps
		// to exactly one path).
		{"https://zddc.varasys.io/releases/archive_stable.html", "https/zddc.varasys.io/releases/archive_stable.html"},
		{"https://ZDDC.Varasys.IO/releases/archive_stable.html", "https/zddc.varasys.io/releases/archive_stable.html"},
		{"http://example.com/foo.html", "http/example.com/foo.html"},
		{"http://example.com:80/foo.html", "http/example.com:80/foo.html"},
		{"https://example.com/foo.html", "https/example.com/foo.html"},
		{"https://example.com:443/foo.html", "https/example.com:443/foo.html"},
		{"https://example.com:8443/foo.html", "https/example.com:8443/foo.html"},
		// Scheme segregation: same host+path under http and https map
		// to different cache entries (defensive against reverse-proxy
		// stacks that legitimately serve different bytes per scheme).
		{"http://example.com/x.html", "http/example.com/x.html"},
		{"https://example.com/x.html", "https/example.com/x.html"},
		// Path normalization preserved.
		{"https://example.com/", "https/example.com/index.html"},
		{"https://example.com", "https/example.com/index.html"},
		{"https://example.com//foo//bar.html", "https/example.com/foo/bar.html"},
	}
	for _, tc := range cases {
		t.Run(tc.raw, func(t *testing.T) {
			got, err := keyForURL(tc.raw)
			if err != nil {
				t.Fatalf("keyForURL error: %v", err)
			}
			if got != tc.want {
				t.Errorf("got %q, want %q", got, tc.want)
			}
		})
	}
}

// TestKeyForURL_NoCollisions: explicit assertion that the dimensions
// previously collapsed (default-port ↔ bare-host, http ↔ https) are
// now distinct. Any future change that re-introduces collapsing will
// fail this test.
func TestKeyForURL_NoCollisions(t *testing.T) {
	pairs := [][2]string{
		// Different scheme, same host+path
		{"http://example.com/x.html", "https://example.com/x.html"},
		// https default port preserved (not collapsed onto bare host)
		{"https://example.com/x.html", "https://example.com:443/x.html"},
		// http default port preserved
		{"http://example.com/x.html", "http://example.com:80/x.html"},
		// Different non-default ports
		{"https://example.com:8443/x.html", "https://example.com:9443/x.html"},
	}
	for _, p := range pairs {
		t.Run(p[0]+" vs "+p[1], func(t *testing.T) {
			a, err := keyForURL(p[0])
			if err != nil {
				t.Fatalf("keyForURL(%q): %v", p[0], err)
			}
			b, err := keyForURL(p[1])
			if err != nil {
				t.Fatalf("keyForURL(%q): %v", p[1], err)
			}
			if a == b {
				t.Errorf("collision: %q and %q both → %q", p[0], p[1], a)
			}
		})
	}
}

func TestKeyForURL_Errors(t *testing.T) {
	cases := []string{
		"",
		"not-a-url",
		"ftp://example.com/x.html",
		"https:///x.html",
		"https://example.com/x.html?v=1",
	}
	for _, tc := range cases {
		t.Run(tc, func(t *testing.T) {
			if _, err := keyForURL(tc); err == nil {
				t.Errorf("keyForURL(%q) = nil, want error", tc)
			}
		})
	}
}

func TestCacheRoundtrip(t *testing.T) {
	c, err := NewCache(filepath.Join(t.TempDir(), "_app"))
	if err != nil {
		t.Fatalf("NewCache: %v", err)
	}
	urlStr := "https://zddc.varasys.io/releases/archive_stable.html"
	body := []byte("<!DOCTYPE html>archive content")

	if c.Has(urlStr) {
		t.Fatalf("Has(empty cache) = true, want false")
	}
	if err := c.Write(urlStr, body); err != nil {
		t.Fatalf("Write: %v", err)
	}
	if !c.Has(urlStr) {
		t.Fatalf("Has(after write) = false, want true")
	}
	got, err := c.Read(urlStr)
	if err != nil {
		t.Fatalf("Read: %v", err)
	}
	if string(got) != string(body) {
		t.Errorf("body mismatch")
	}
}

func TestCacheAtomicWrite_LeavesNoTempOnSuccess(t *testing.T) {
	root := filepath.Join(t.TempDir(), "_app")
	c, _ := NewCache(root)
	urlStr := "https://zddc.varasys.io/releases/archive_stable.html"
	if err := c.Write(urlStr, []byte("hello")); err != nil {
		t.Fatalf("Write: %v", err)
	}
	count := 0
	_ = filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() && strings.Contains(info.Name(), ".tmp.") {
			count++
		}
		return nil
	})
	if count != 0 {
		t.Errorf("found %d .tmp.* leftovers, want 0", count)
	}
}

func TestCacheSweepsTempsOnNew(t *testing.T) {
	root := filepath.Join(t.TempDir(), "_app")
	stale := filepath.Join(root, "example.com", "releases", "archive_stable.html.tmp.123")
	if err := os.MkdirAll(filepath.Dir(stale), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(stale, []byte("partial"), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := NewCache(root); err != nil {
		t.Fatalf("NewCache: %v", err)
	}
	if _, err := os.Stat(stale); !os.IsNotExist(err) {
		t.Errorf("stale tmp file not swept: %v", err)
	}
}
