package main

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
)

func TestLookupMissingDomain(t *testing.T) {
	s := &server{
		nrenASNs: make(map[uint]struct{}),
	}
	s.ready.Store(true)

	req := httptest.NewRequest(http.MethodGet, "/lookup", nil)
	rr := httptest.NewRecorder()

	s.lookupHandler(rr, req)

	if rr.Code != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d", rr.Code)
	}

	if !strings.Contains(rr.Body.String(), "missing domain") {
		t.Fatalf("expected error message in response")
	}
}

func TestLookupServiceNotReady(t *testing.T) {
	s := &server{
		nrenASNs: make(map[uint]struct{}),
	}
	s.ready = atomic.Bool{}
	s.ready.Store(false)

	req := httptest.NewRequest(http.MethodGet, "/lookup?domain=example.com", nil)
	rr := httptest.NewRecorder()

	s.lookupHandler(rr, req)

	if rr.Code != http.StatusServiceUnavailable {
		t.Fatalf("expected 503, got %d", rr.Code)
	}
}

func TestMatchesUniDomain(t *testing.T) {
	set := map[string]struct{}{
		"uni-stuttgart.de": {},
		"hdm-stuttgart.de": {},
	}
	tests := []struct {
		domain    string
		wantMatch bool
		wantEntry string
	}{
		{"uni-stuttgart.de", true, "uni-stuttgart.de"},        // exact match
		{"insti.uni-stuttgart.de", true, "uni-stuttgart.de"}, // single-level subdomain
		{"a.b.uni-stuttgart.de", true, "uni-stuttgart.de"},   // multi-level subdomain
		{"evil-uni-stuttgart.de", false, ""},                  // lookalike non-match (different label)
		{"example.com", false, ""},                            // not in set
		{"com", false, ""},                                    // single-label input
		{"uni-stuttgart.de.", true, "uni-stuttgart.de"},       // trailing dot normalised
		{"", false, ""},                                       // empty string
	}
	for _, tc := range tests {
		got, entry := matchesUniDomain(tc.domain, set)
		if got != tc.wantMatch || entry != tc.wantEntry {
			t.Errorf("matchesUniDomain(%q): got (%v, %q), want (%v, %q)",
				tc.domain, got, entry, tc.wantMatch, tc.wantEntry)
		}
	}
}

func TestExtractDomain(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{"uni-stuttgart.de", "uni-stuttgart.de"},             // plain domain passthrough
		{"foo@uni-stuttgart.de", "uni-stuttgart.de"},         // email extraction
		{"FOO@UNI-STUTTGART.DE", "uni-stuttgart.de"},         // uppercase normalisation
		{"  foo@uni-stuttgart.de  ", "uni-stuttgart.de"},     // leading/trailing spaces
		{"notanemail", "notanemail"},                          // no-@ passthrough
	}
	for _, tc := range tests {
		got := extractDomain(tc.input)
		if got != tc.want {
			t.Errorf("extractDomain(%q): got %q, want %q", tc.input, got, tc.want)
		}
	}
}

func TestLookupDomainMatch(t *testing.T) {
	s := &server{
		nrenASNs:   map[uint]struct{}{},
		uniDomains: map[string]struct{}{"uni-stuttgart.de": {}},
	}
	s.ready.Store(true)

	req := httptest.NewRequest(http.MethodGet, "/lookup?domain=insti.uni-stuttgart.de", nil)
	rr := httptest.NewRecorder()
	s.lookupHandler(rr, req)

	body := rr.Body.String()
	if !strings.Contains(body, `"domain_match":true`) {
		t.Errorf("expected domain_match:true in %s", body)
	}
	if !strings.Contains(body, `"matched_domain":"uni-stuttgart.de"`) {
		t.Errorf("expected matched_domain in %s", body)
	}
	if !strings.Contains(body, `"nren":true`) {
		t.Errorf("expected nren:true in %s", body)
	}
	if !strings.Contains(body, `"asn_match":false`) {
		t.Errorf("expected asn_match:false in %s", body)
	}
}

func TestLookupEmailParam(t *testing.T) {
	s := &server{
		nrenASNs:   map[uint]struct{}{},
		uniDomains: map[string]struct{}{"uni-stuttgart.de": {}},
	}
	s.ready.Store(true)

	req := httptest.NewRequest(http.MethodGet, "/lookup?email=student%40insti.uni-stuttgart.de", nil)
	rr := httptest.NewRecorder()
	s.lookupHandler(rr, req)

	body := rr.Body.String()
	if !strings.Contains(body, `"domain_match":true`) {
		t.Errorf("expected domain_match:true in %s", body)
	}
	if !strings.Contains(body, `"matched_domain":"uni-stuttgart.de"`) {
		t.Errorf("expected matched_domain in %s", body)
	}
	if !strings.Contains(body, `"nren":true`) {
		t.Errorf("expected nren:true in %s", body)
	}
}

func TestLookupEmailPrecedence(t *testing.T) {
	s := &server{
		nrenASNs:   map[uint]struct{}{},
		uniDomains: map[string]struct{}{"uni-stuttgart.de": {}},
	}
	s.ready.Store(true)

	// email= takes precedence over domain=; example.com is not in uniDomains
	req := httptest.NewRequest(http.MethodGet, "/lookup?email=a%40uni-stuttgart.de&domain=example.com", nil)
	rr := httptest.NewRecorder()
	s.lookupHandler(rr, req)

	body := rr.Body.String()
	if !strings.Contains(body, `"domain":"uni-stuttgart.de"`) {
		t.Errorf("expected domain uni-stuttgart.de (from email param) in %s", body)
	}
	if !strings.Contains(body, `"nren":true`) {
		t.Errorf("expected nren:true in %s", body)
	}
}

func TestHealthzJSON(t *testing.T) {
	s := &server{
		asnCount:   42,
		minASN:     10,
		uniDomains: map[string]struct{}{"uni-stuttgart.de": {}, "hdm-stuttgart.de": {}},
	}

	req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
	rr := httptest.NewRecorder()
	s.healthzHandler(rr, req)

	if rr.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rr.Code)
	}
	body := rr.Body.String()
	if !strings.Contains(body, `"asn_count":42`) {
		t.Errorf("expected asn_count:42 in %s", body)
	}
	if !strings.Contains(body, `"domain_count":2`) {
		t.Errorf("expected domain_count:2 in %s", body)
	}
	if ct := rr.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") {
		t.Errorf("expected Content-Type application/json, got %s", ct)
	}
}

func TestLookupEmailNoAt(t *testing.T) {
	s := &server{
		nrenASNs:   map[uint]struct{}{},
		uniDomains: map[string]struct{}{},
	}
	s.ready.Store(true)

	req := httptest.NewRequest(http.MethodGet, "/lookup?email=notanemail", nil)
	rr := httptest.NewRecorder()
	s.lookupHandler(rr, req)

	if rr.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rr.Code)
	}
	if !strings.Contains(rr.Body.String(), "email param has no @") {
		t.Fatalf("expected error message in response, got: %s", rr.Body.String())
	}
}
