package tokenanalysis

import (
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"io/fs"
	"net/http"
	"net/http/httptest"
	"testing"
	"testing/fstest"

	"github.com/stretchr/testify/require"
)

func testUIServer(t *testing.T) *uiServer {
	t.Helper()
	assets := fstest.MapFS{
		"index.html": &fstest.MapFile{Data: []byte("<!doctype html>")},
		"app.js":     &fstest.MapFile{Data: []byte(`"use strict";`)},
		"styles.css": &fstest.MapFile{Data: []byte("body{}")},
	}
	var assetFS fs.FS = assets
	local := comparisonReport(120, nil)
	local.Collection.Enabled = false
	local.DataQuality = DataQuality{State: StateDisabled, Diagnostics: []string{"attribution_disabled"}}
	local.Agents = []AgentSummary{{
		AgentRef: "agent_0000000000000001", DisplayName: "Private Agent", Runtime: "codex", Model: "gpt-private",
	}}
	local.Fingerprint = reportFingerprint(local)
	body, err := json.Marshal(local)
	require.NoError(t, err)
	return &uiServer{
		token:  "process-secret",
		origin: "http://127.0.0.1:43001",
		client: Client{
			BaseURL: "http://127.0.0.1:43002",
			APIKey:  "daemon-key",
			HTTP: &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
				return &http.Response{
					StatusCode: http.StatusOK,
					Header:     make(http.Header),
					Body:       io.NopCloser(bytes.NewReader(body)),
					Request:    request,
				}, nil
			})},
		},
		assets: assetFS,
	}
}

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

func (fn roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return fn(request)
}

func performUIRequest(server *uiServer, method, target, remoteAddress string, body []byte) *httptest.ResponseRecorder {
	request := httptest.NewRequest(method, target, bytes.NewReader(body))
	request.Host = "127.0.0.1:43001"
	request.RemoteAddr = remoteAddress
	response := httptest.NewRecorder()
	server.ServeHTTP(response, request)
	return response
}

func TestUIServerRejectsNonLoopbackAndForeignOrigin(t *testing.T) {
	server := testUIServer(t)

	response := performUIRequest(server, http.MethodGet, "http://127.0.0.1:43001/", "192.0.2.8:55000", nil)
	require.Equal(t, http.StatusForbidden, response.Code)

	request := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:43001/", nil)
	request.Host = "127.0.0.1:43001"
	request.RemoteAddr = "127.0.0.1:55000"
	request.Header.Set("Origin", "https://attacker.example")
	response = httptest.NewRecorder()
	server.ServeHTTP(response, request)
	require.Equal(t, http.StatusForbidden, response.Code)
}

func TestUIServerRequiresBearerAndSetsSecurityHeaders(t *testing.T) {
	server := testUIServer(t)

	response := performUIRequest(server, http.MethodGet, "http://127.0.0.1:43001/", "127.0.0.1:55000", nil)
	require.Equal(t, http.StatusOK, response.Code)
	require.Equal(t, "no-store", response.Header().Get("Cache-Control"))
	require.Contains(t, response.Header().Get("Content-Security-Policy"), "default-src 'none'")
	require.Equal(t, "nosniff", response.Header().Get("X-Content-Type-Options"))

	response = performUIRequest(server, http.MethodGet, "http://127.0.0.1:43001/api/analysis", "127.0.0.1:55000", nil)
	require.Equal(t, http.StatusUnauthorized, response.Code)
}

func TestUIServerAnalysisUsesPassiveDaemonRead(t *testing.T) {
	server := testUIServer(t)
	request := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:43001/api/analysis?window=1d", nil)
	request.Host = "127.0.0.1:43001"
	request.RemoteAddr = "127.0.0.1:55000"
	request.Header.Set("Authorization", "Bearer process-secret")
	response := httptest.NewRecorder()

	server.ServeHTTP(response, request)

	require.Equal(t, http.StatusOK, response.Code, response.Body.String())
	require.Contains(t, response.Body.String(), `"enabled":false`)
	require.NotContains(t, response.Body.String(), "sourcePath")
}

func TestUIServerCollectionRejectsTrailingJSON(t *testing.T) {
	server := testUIServer(t)
	request := httptest.NewRequest(
		http.MethodPut,
		"http://127.0.0.1:43001/api/collection",
		bytes.NewBufferString(`{"enabled":true}{"enabled":false}`),
	)
	request.Host = "127.0.0.1:43001"
	request.RemoteAddr = "127.0.0.1:55000"
	request.Header.Set("Authorization", "Bearer process-secret")
	response := httptest.NewRecorder()
	server.ServeHTTP(response, request)
	require.Equal(t, http.StatusBadRequest, response.Code)
}

func TestClientMintsFreshHumanCredentialForEveryRequest(t *testing.T) {
	var credentials []string
	client := Client{
		BaseURL: "http://127.0.0.1:43002",
		APIKeyProvider: func() (string, error) {
			return "daemon-key-" + string(rune('a'+len(credentials))), nil
		},
		HTTP: &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			credentials = append(credentials, request.Header.Get("X-Api-Key"))
			body, err := json.Marshal(CollectionStatus{Enabled: true})
			require.NoError(t, err)
			return &http.Response{
				StatusCode: http.StatusOK,
				Header:     make(http.Header),
				Body:       io.NopCloser(bytes.NewReader(body)),
				Request:    request,
			}, nil
		})},
	}

	_, err := client.collection(t.Context(), http.MethodGet, nil)
	require.NoError(t, err)
	_, err = client.collection(t.Context(), http.MethodGet, nil)
	require.NoError(t, err)
	require.Equal(t, []string{"daemon-key-a", "daemon-key-b"}, credentials)

	client.APIKeyProvider = func() (string, error) { return "", errors.New("unavailable") }
	_, err = client.collection(t.Context(), http.MethodGet, nil)
	require.ErrorContains(t, err, "create local Token analysis credential")
}

func TestUIServerPortableExportAndComparisonRejectLocalMetadata(t *testing.T) {
	server := testUIServer(t)
	request := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:43001/api/analysis?window=1d&portable=true", nil)
	request.Host = "127.0.0.1:43001"
	request.RemoteAddr = "127.0.0.1:55000"
	request.Header.Set("Authorization", "Bearer process-secret")
	response := httptest.NewRecorder()
	server.ServeHTTP(response, request)
	require.Equal(t, http.StatusOK, response.Code, response.Body.String())

	var portable Report
	require.NoError(t, json.Unmarshal(response.Body.Bytes(), &portable))
	require.NoError(t, ValidatePortableReport(portable))
	require.NotEmpty(t, portable.Comparability.RuntimeFingerprint)
	require.NotContains(t, response.Body.String(), "Private Agent")
	require.NotContains(t, response.Body.String(), "gpt-private")
	require.Empty(t, portable.Agents)
	require.Empty(t, portable.Runs)

	compareBody, err := json.Marshal(struct {
		Baseline  Report `json:"baseline"`
		Candidate Report `json:"candidate"`
	}{Baseline: portable, Candidate: portable})
	require.NoError(t, err)
	compareRequest := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:43001/api/compare", bytes.NewReader(compareBody))
	compareRequest.Host = "127.0.0.1:43001"
	compareRequest.RemoteAddr = "127.0.0.1:55000"
	compareRequest.Header.Set("Authorization", "Bearer process-secret")
	compareResponse := httptest.NewRecorder()
	server.ServeHTTP(compareResponse, compareRequest)
	require.Equal(t, http.StatusOK, compareResponse.Code, compareResponse.Body.String())

	malicious := portable
	malicious.Agents = []AgentSummary{{AgentRef: "agent_0000000000000001"}}
	malicious.Fingerprint = reportFingerprint(malicious)
	compareBody, err = json.Marshal(struct {
		Baseline  Report `json:"baseline"`
		Candidate Report `json:"candidate"`
	}{Baseline: malicious, Candidate: portable})
	require.NoError(t, err)
	compareRequest = httptest.NewRequest(http.MethodPost, "http://127.0.0.1:43001/api/compare", bytes.NewReader(compareBody))
	compareRequest.Host = "127.0.0.1:43001"
	compareRequest.RemoteAddr = "127.0.0.1:55000"
	compareRequest.Header.Set("Authorization", "Bearer process-secret")
	compareResponse = httptest.NewRecorder()
	server.ServeHTTP(compareResponse, compareRequest)
	require.Equal(t, http.StatusBadRequest, compareResponse.Code)
	require.Contains(t, compareResponse.Body.String(), "machine-local")
}
