package services

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/chromedp/cdproto/fetch"
	"github.com/chromedp/cdproto/network"
	"golang.org/x/net/html"
)

func TestCrawlContentRejectsLoadingBeforeTraining(t *testing.T) {
	for _, content := range []string{"", "Memuat toko…", "Loading products...", "Toko ini menyediakan kebutuhan rumah tangga untuk pelanggan.\nMemuat…"} {
		if CrawlContentIssue(content) == "" {
			t.Fatalf("unusable content accepted: %q", content)
		}
	}
	if CrawlContentIssue("Panduan memuat data pelanggan dari file Excel ke dalam aplikasi toko.") != "" {
		t.Fatal("ordinary prose mentioning loading was rejected")
	}
}

func TestCrawlBrowserDiscoversDynamicProductLinks(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/robots.txt" {
			fmt.Fprint(w, "User-agent: *\nDisallow: /private\n")
			return
		}
		w.Header().Set("Content-Type", "text/html")
		fmt.Fprint(w, `<title>Olshop</title><main>Memuat toko…</main><script src="/app.js"></script>`)
	}))
	defer server.Close()
	renders, pages := 0, 0
	renderer := func(ctx context.Context, target string) (crawlRenderedPage, error) {
		renders++
		return crawlRenderedPage{URL: target, HTML: `<title>SBB shop</title><main><h1>Katalog kebutuhan rumah tangga</h1><p>Galon Club tukar tersedia dengan harga Rp26.000.</p><a href="/product?id=1">Galon</a><a href="/product?id=2">Rak sepatu</a><a href="/private">Private</a></main>`}, nil
	}
	err := crawlWebsiteWithRenderer(context.Background(), server.URL, nil, 5, server.Client(), 0, renderer, func(target, title, content string, err error) {
		if strings.HasSuffix(target, "/private") {
			if err == nil {
				t.Error("robots restriction bypassed")
			}
			return
		}
		pages++
		if err != nil || title != "SBB shop" || !strings.Contains(content, "Rp26.000") {
			t.Errorf("dynamic content not saved: title=%q content=%q err=%v", title, content, err)
		}
	})
	if err != nil || pages != 3 || renders != 3 {
		t.Fatalf("err=%v pages=%d renders=%d", err, pages, renders)
	}
}

func TestCrawlBrowserResourcePolicy(t *testing.T) {
	base, _ := url.Parse("https://shop.example")
	for _, test := range []struct{ target, method string }{
		{"http://127.0.0.1:3030/secrets", "GET"},
		{"http://[::1]/", "GET"},
		{"http://169.254.169.254/latest/meta-data/", "GET"},
		{"file:///etc/passwd", "GET"},
		{"https://user:secret@shop.example", "GET"},
		{"https://shop.example/orders", "POST"},
		{"https://shop.example/orders/1", "DELETE"},
	} {
		event := &fetch.EventRequestPaused{Request: &network.Request{URL: test.target, Method: test.method}, ResourceType: network.ResourceTypeFetch}
		if _, _, err := fetchCrawlBrowserResource(context.Background(), newCrawlHTTPClient(), base, event); err == nil {
			t.Errorf("unsafe request accepted: %s %s", test.method, test.target)
		}
	}
}

type crawlTestTransport func(*http.Request) (*http.Response, error)

func (f crawlTestTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }

// Opt-in local Chrome test. All fixture requests are mapped to a test server;
// other hosts still go through the production public-address transport.
func TestCrawlBrowserRendersDelayedContentWithoutUnsafeRequests(t *testing.T) {
	if os.Getenv("CHATLOOP_BROWSER_TEST") != "1" {
		t.Skip("set CHATLOOP_BROWSER_TEST=1 with Chrome installed")
	}
	var apiCalls, blockedHits atomic.Int64
	private := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { blockedHits.Add(1) }))
	defer private.Close()
	fixture := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "GET" || r.Header.Get("Authorization") != "" || r.Header.Get("Cookie") != "" {
			blockedHits.Add(1)
		}
		if r.URL.Path == "/api/catalog" {
			apiCalls.Add(1)
			time.Sleep(1600 * time.Millisecond)
			w.Header().Set("Content-Type", "application/json")
			fmt.Fprint(w, `{"name":"Rak sepatu Capella","price":"Rp694.000"}`)
			return
		}
		w.Header().Set("Content-Type", "text/html")
		fmt.Fprintf(w, `<title>Olshop</title><main>Memuat toko…</main><iframe src="https://external-frame.example/"></iframe><script>
		fetch('/write', {method:'POST', body:'no'}).catch(()=>{});
		fetch(%q).catch(()=>{});
		new WebSocket(%q).onerror=()=>{};
		fetch('/api/catalog', {headers:{Authorization:'secret',Cookie:'session=secret'}}).then(r=>r.json()).then(p=>{
		 document.title='Toko fixture'; document.querySelector('main').innerHTML='<h1>'+p.name+'</h1><p>Harga '+p.price+'. Produk tersedia untuk kebutuhan penyimpanan rumah tangga.</p><a href="/product?id=42">Rincian produk</a>';
		});</script>`, private.URL, strings.Replace(private.URL, "http:", "ws:", 1))
	}))
	defer fixture.Close()
	fixtureURL, _ := url.Parse(fixture.URL)
	safe := newCrawlHTTPClient()
	defer safe.CloseIdleConnections()
	client := &http.Client{Timeout: crawlTimeout, Transport: crawlTestTransport(func(req *http.Request) (*http.Response, error) {
		if req.URL.Hostname() != "crawl-fixture.example" {
			return safe.Transport.RoundTrip(req)
		}
		clone := req.Clone(req.Context())
		clone.URL.Scheme, clone.URL.Host = fixtureURL.Scheme, fixtureURL.Host
		return fixture.Client().Transport.RoundTrip(clone)
	})}
	ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second)
	defer cancel()
	result, err := renderCrawlPageUsing(ctx, "http://crawl-fixture.example/", client)
	if err != nil {
		t.Fatal(err)
	}
	doc, _ := html.Parse(strings.NewReader(result.HTML))
	title, content := extractTitleText(doc)
	if title != "Toko fixture" || !strings.Contains(content, "Rp694.000") || crawlHasLoadingText(content) || apiCalls.Load() != 1 || blockedHits.Load() != 0 {
		t.Fatalf("title=%s content=%s api=%d unsafe=%d", title, content, apiCalls.Load(), blockedHits.Load())
	}
}

func TestCrawlBrowserUnavailableReturnsActionableError(t *testing.T) {
	t.Setenv("CRAWL_CHROME_PATH", t.TempDir()+"/missing-browser")
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	result, err := renderCrawlPage(ctx, "https://example.com")
	if err == nil || !strings.Contains(err.Error(), "Chrome atau Chromium") || result.HTML != "" {
		t.Fatalf("unavailable reader did not fail clearly: result=%+v err=%v", result, err)
	}
}

func TestCrawlBrowserUnresolvedShellIsNotSuccessful(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/robots.txt" {
			w.WriteHeader(404)
			return
		}
		w.Header().Set("Content-Type", "text/html")
		fmt.Fprint(w, `<main>Memuat toko…</main><script>load()</script>`)
	}))
	defer server.Close()
	render := func(_ context.Context, target string) (crawlRenderedPage, error) {
		return crawlRenderedPage{URL: target, HTML: `<main>Memuat toko…</main>`}, nil
	}
	var failure error
	_ = crawlWebsiteWithRenderer(context.Background(), server.URL, nil, 1, server.Client(), 0, render, func(_, _, _ string, err error) { failure = err })
	if failure == nil || !strings.Contains(failure.Error(), "loading") {
		t.Fatalf("shell accepted: %v", failure)
	}
}

// Explicit opt-in: only public website reads, no DB writes, AI calls, or WA.
func TestCrawlBrowserLiveWebsite(t *testing.T) {
	root := os.Getenv("CHATLOOP_LIVE_CRAWL_URL")
	if root == "" {
		t.Skip("set CHATLOOP_LIVE_CRAWL_URL to test a public website")
	}
	readable, products := 0, 0
	err := crawlWebsite(root, nil, 4, newCrawlHTTPClient(), crawlDelay, func(target, title, content string, err error) {
		if err != nil {
			t.Errorf("%s: %v", target, err)
			return
		}
		readable++
		if strings.Contains(target, "/product?") {
			products++
		}
		t.Logf("url=%s title=%q chars=%d recommended=%t", target, title, len([]rune(content)), ScorePageForCSTraining(target, title, content).Recommended)
	})
	if err != nil || readable < 2 || products == 0 {
		t.Fatalf("err=%v readable=%d products=%d", err, readable, products)
	}
}
