package dorks

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
	"time"
)

func sampleDork() Dork {
	return Dork{
		ID:     "openai-github-envfile",
		Name:   "OpenAI key in .env",
		Source: "github",
		Query:  "sk-proj- extension:env",
	}
}

func newTestExecutor(token, baseURL string) *GitHubExecutor {
	return &GitHubExecutor{
		Token:      token,
		BaseURL:    baseURL,
		HTTPClient: &http.Client{Timeout: 5 * time.Second},
		MaxRetries: 1,
	}
}

func TestGitHubExecutor_Source(t *testing.T) {
	g := NewGitHubExecutor("x")
	if g.Source() != "github" {
		t.Fatalf("expected source %q, got %q", "github", g.Source())
	}
}

func TestGitHubExecutor_MissingTokenReturnsErrMissingAuth(t *testing.T) {
	// Use a server that would fail the test if it were ever hit — the
	// executor must short-circuit before issuing an HTTP request.
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Fatalf("server should not be hit when token is empty; got %s %s", r.Method, r.URL.Path)
	}))
	defer srv.Close()

	g := newTestExecutor("", srv.URL)
	_, err := g.Execute(context.Background(), sampleDork(), 10)
	if err == nil {
		t.Fatal("expected error for empty token")
	}
	if !errors.Is(err, ErrMissingAuth) {
		t.Fatalf("expected ErrMissingAuth, got %v", err)
	}
	if !strings.Contains(err.Error(), "GITHUB_TOKEN") {
		t.Fatalf("expected setup instructions in error, got %q", err.Error())
	}
}

func TestGitHubExecutor_SuccessfulSearchParsesMatches(t *testing.T) {
	body := `{
		"total_count": 2,
		"items": [
			{
				"name": ".env",
				"path": "backend/.env",
				"html_url": "https://github.com/acme/leaky/blob/main/backend/.env",
				"repository": {"full_name": "acme/leaky"},
				"text_matches": [{"fragment": "OPENAI_API_KEY=sk-proj-AAA"}]
			},
			{
				"name": "config.env",
				"path": "infra/config.env",
				"html_url": "https://github.com/acme/other/blob/main/infra/config.env",
				"repository": {"full_name": "acme/other"},
				"text_matches": [{"fragment": "SECRET=sk-proj-BBB"}]
			}
		]
	}`
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got := r.Header.Get("Authorization"); got != "Bearer test-token" {
			t.Errorf("expected Bearer auth, got %q", got)
		}
		if got := r.Header.Get("Accept"); !strings.Contains(got, "text-match") {
			t.Errorf("expected text-match Accept header, got %q", got)
		}
		if r.URL.Path != "/search/code" {
			t.Errorf("expected /search/code, got %s", r.URL.Path)
		}
		if q := r.URL.Query().Get("q"); q != "sk-proj- extension:env" {
			t.Errorf("expected query to be url-decoded to original, got %q", q)
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(body))
	}))
	defer srv.Close()

	g := newTestExecutor("test-token", srv.URL)
	matches, err := g.Execute(context.Background(), sampleDork(), 10)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(matches) != 2 {
		t.Fatalf("expected 2 matches, got %d", len(matches))
	}
	m := matches[0]
	if m.DorkID != "openai-github-envfile" {
		t.Errorf("DorkID = %q", m.DorkID)
	}
	if m.Source != "github" {
		t.Errorf("Source = %q", m.Source)
	}
	if m.URL != "https://github.com/acme/leaky/blob/main/backend/.env" {
		t.Errorf("URL = %q", m.URL)
	}
	if m.Path != "acme/leaky/backend/.env" {
		t.Errorf("Path = %q", m.Path)
	}
	if m.Snippet != "OPENAI_API_KEY=sk-proj-AAA" {
		t.Errorf("Snippet = %q", m.Snippet)
	}
}

func TestGitHubExecutor_LimitCapsResults(t *testing.T) {
	// Build a response with 10 items; executor must return only 5.
	var items []string
	for i := 0; i < 10; i++ {
		items = append(items, fmt.Sprintf(`{
			"name": "f%d.env",
			"path": "dir/f%d.env",
			"html_url": "https://github.com/acme/repo/blob/main/dir/f%d.env",
			"repository": {"full_name": "acme/repo"},
			"text_matches": [{"fragment": "sk-proj-%d"}]
		}`, i, i, i, i))
	}
	body := fmt.Sprintf(`{"total_count": 10, "items": [%s]}`, strings.Join(items, ","))

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got := r.URL.Query().Get("per_page"); got != "5" {
			t.Errorf("expected per_page=5, got %q", got)
		}
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(body))
	}))
	defer srv.Close()

	g := newTestExecutor("test-token", srv.URL)
	matches, err := g.Execute(context.Background(), sampleDork(), 5)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(matches) != 5 {
		t.Fatalf("expected 5 matches after cap, got %d", len(matches))
	}
}

func TestGitHubExecutor_RetryAfterSleepsAndRetries(t *testing.T) {
	var hits int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		n := atomic.AddInt32(&hits, 1)
		if n == 1 {
			w.Header().Set("Retry-After", "1")
			w.Header().Set("X-RateLimit-Remaining", "0")
			w.WriteHeader(http.StatusForbidden)
			_, _ = w.Write([]byte(`{"message":"rate limit"}`))
			return
		}
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"total_count":1,"items":[{
			"name":"a.env","path":"a.env","html_url":"https://github.com/x/y/blob/main/a.env",
			"repository":{"full_name":"x/y"},
			"text_matches":[{"fragment":"sk-proj-ZZ"}]}]}`))
	}))
	defer srv.Close()

	g := newTestExecutor("test-token", srv.URL)
	start := time.Now()
	matches, err := g.Execute(context.Background(), sampleDork(), 10)
	elapsed := time.Since(start)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(matches) != 1 {
		t.Fatalf("expected 1 match after retry, got %d", len(matches))
	}
	if atomic.LoadInt32(&hits) != 2 {
		t.Fatalf("expected 2 server hits, got %d", hits)
	}
	if elapsed < 900*time.Millisecond {
		t.Fatalf("expected to sleep ~1s from Retry-After, only waited %s", elapsed)
	}
}

func TestGitHubExecutor_RateLimitExhaustedReturnsError(t *testing.T) {
	var hits int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&hits, 1)
		w.Header().Set("Retry-After", "0")
		w.WriteHeader(http.StatusTooManyRequests)
		_, _ = w.Write([]byte(`{"message":"secondary rate limit"}`))
	}))
	defer srv.Close()

	g := newTestExecutor("test-token", srv.URL)
	g.MaxRetries = 1
	_, err := g.Execute(context.Background(), sampleDork(), 10)
	if err == nil {
		t.Fatal("expected rate limit error")
	}
	if !strings.Contains(err.Error(), "rate limit") {
		t.Fatalf("expected rate limit in error, got %q", err.Error())
	}
	if atomic.LoadInt32(&hits) != 2 {
		t.Fatalf("expected 2 hits (initial + 1 retry), got %d", hits)
	}
}

func TestGitHubExecutor_UnauthorizedMapsToMissingAuth(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
		_, _ = w.Write([]byte(`{"message":"Bad credentials"}`))
	}))
	defer srv.Close()

	g := newTestExecutor("bad-token", srv.URL)
	_, err := g.Execute(context.Background(), sampleDork(), 10)
	if err == nil {
		t.Fatal("expected error")
	}
	if !errors.Is(err, ErrMissingAuth) {
		t.Fatalf("expected ErrMissingAuth wrap, got %v", err)
	}
}

func TestGitHubExecutor_UnprocessableEntityReturnsDescriptiveError(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusUnprocessableEntity)
		_, _ = w.Write([]byte(`{"message":"Validation Failed"}`))
	}))
	defer srv.Close()

	g := newTestExecutor("test-token", srv.URL)
	_, err := g.Execute(context.Background(), sampleDork(), 10)
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "422") {
		t.Fatalf("expected status code in error, got %q", err.Error())
	}
	if !strings.Contains(err.Error(), "Validation Failed") {
		t.Fatalf("expected server message in error, got %q", err.Error())
	}
}

func TestParseRetryAfter(t *testing.T) {
	cases := map[string]time.Duration{
		"":     time.Second,
		"0":    time.Second,
		"1":    time.Second,
		"5":    5 * time.Second,
		"huh?": time.Second,
	}
	for in, want := range cases {
		if got := parseRetryAfter(in); got != want {
			t.Errorf("parseRetryAfter(%q) = %s, want %s", in, got, want)
		}
	}
}
