package tools

import (
	"context"
	"strings"
	"testing"
	"time"
)

func TestInteractshSession_PayloadUniqueUnderBase(t *testing.T) {
	s := NewInteractshSession()
	s.base = "c6a1b2c3.oast.fun" // as Start would set it

	url1, tok1, err := s.Payload(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	url2, tok2, _ := s.Payload(context.Background())

	if tok1 == tok2 {
		t.Error("two Payload calls must mint distinct tokens")
	}
	for _, u := range []string{url1, url2} {
		if !strings.HasSuffix(u, "."+s.base) {
			t.Errorf("payload url %q is not a subdomain of the base host", u)
		}
		if !strings.HasPrefix(u, "http://") {
			t.Errorf("payload url %q missing scheme", u)
		}
	}
	if !strings.Contains(url1, tok1) {
		t.Errorf("url %q should embed its token %q", url1, tok1)
	}
}

func TestInteractshSession_PayloadBeforeStartErrors(t *testing.T) {
	s := NewInteractshSession()
	if _, _, err := s.Payload(context.Background()); err == nil {
		t.Error("Payload before Start should error (no base host yet)")
	}
}

func TestInteractshSession_ObservedHitRendersProof(t *testing.T) {
	s := NewInteractshSession()
	s.base = "c6a1b2c3.oast.fun"
	token := "deadbeef01"
	s.raw = []string{
		`{"protocol":"dns","unique-id":"c6a1b2c3","full-id":"deadbeef01.c6a1b2c3.oast.fun","remote-address":"203.0.113.9","timestamp":"2026-10-16T10:00:00Z"}`,
	}

	hit, proof, err := s.Observed(context.Background(), token, 100*time.Millisecond)
	if err != nil || !hit {
		t.Fatalf("hit=%v err=%v, want true/nil", hit, err)
	}
	for _, want := range []string{"DNS", "203.0.113.9", "deadbeef01"} {
		if !strings.Contains(proof, want) {
			t.Errorf("proof %q missing %q", proof, want)
		}
	}
}

func TestInteractshSession_ObservedSilenceIsNoHit(t *testing.T) {
	s := NewInteractshSession()
	s.base = "c6a1b2c3.oast.fun"
	s.raw = []string{
		`{"protocol":"http","full-id":"somethingelse.c6a1b2c3.oast.fun","remote-address":"198.51.100.2"}`,
	}
	start := time.Now()
	hit, _, err := s.Observed(context.Background(), "deadbeef01", 60*time.Millisecond)
	if err != nil {
		t.Fatal(err)
	}
	if hit {
		t.Error("a callback for a different token must not count as a hit")
	}
	if time.Since(start) < 50*time.Millisecond {
		t.Error("Observed should wait out the window before reporting silence")
	}
}

func TestInteractshSession_ObservedContextCancel(t *testing.T) {
	s := NewInteractshSession()
	s.base = "c6a1b2c3.oast.fun"
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	if _, _, err := s.Observed(ctx, "x", time.Second); err == nil {
		t.Error("cancelled context should return an error")
	}
}

func TestRandLabel_ValidDNSLabel(t *testing.T) {
	seen := map[string]bool{}
	for i := 0; i < 100; i++ {
		l := randLabel(10)
		if len(l) != 10 {
			t.Fatalf("label %q len = %d, want 10", l, len(l))
		}
		for _, r := range l {
			if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')) {
				t.Fatalf("label %q has non-hex char %q", l, r)
			}
		}
		if seen[l] {
			t.Fatalf("duplicate label %q within 100 draws", l)
		}
		seen[l] = true
	}
}

func TestInteractshSession_CloseIdempotent(t *testing.T) {
	s := NewInteractshSession()
	s.Close()
	s.Close() // must not panic on a never-started / double close
}
