package content

import (
	"archive/zip"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	"github.com/tidbcloud/AgentTeam/internal/identity/trusted"
	service "github.com/tidbcloud/AgentTeam/internal/service/content"
	store "github.com/tidbcloud/AgentTeam/internal/store/content"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func newAPITestHandler(t *testing.T) http.Handler {
	t.Helper()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&store.VersionedContent{}, &store.ContentVersion{}, &store.VersionPath{}, &store.VersionPin{}, &store.Outbox{}, &store.StagedPayloadGC{}))
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	sequence := 0
	contentCore, err := service.NewTree(store.New(db), tree, func(prefix string) (string, error) {
		sequence++
		return fmt.Sprintf("%s_%d", prefix, sequence), nil
	})
	require.NoError(t, err)
	contentService, err := service.NewAuthorizedService(contentCore, allowContentAccessPolicy{})
	require.NoError(t, err)
	mux := http.NewServeMux()
	RegisterRoutes(mux, New(contentService), nil)
	return mux
}

type allowContentAccessPolicy struct{}

func (allowContentAccessPolicy) AuthorizeContentAccess(context.Context, service.AccessRequest) error {
	return nil
}

func TestContentStoreHTTPVerticalSlice(t *testing.T) {
	handler := newAPITestHandler(t)
	create := doJSON(t, handler, http.MethodPost, "/api/v1/content-store/contents", `{"domain":"skill","bindingKey":"skill_1"}`)
	require.Equal(t, http.StatusCreated, create.Code, create.Body.String())
	var created struct {
		ID         string `json:"id"`
		Generation uint64 `json:"generation"`
	}
	require.NoError(t, json.Unmarshal(create.Body.Bytes(), &created))
	require.NotEmpty(t, created.ID)
	require.Equal(t, uint64(1), created.Generation)

	commitBody := `{"expectedGeneration":1,"files":[{"path":"SKILL.md","contentBase64":"aGVsbG8="}]}`
	commit := doJSON(t, handler, http.MethodPost, "/api/v1/content-store/contents/"+created.ID+"/versions", commitBody)
	require.Equal(t, http.StatusCreated, commit.Code, commit.Body.String())
	var committed struct {
		Version struct {
			ID string `json:"id"`
		} `json:"version"`
	}
	require.NoError(t, json.Unmarshal(commit.Body.Bytes(), &committed))
	require.NotEmpty(t, committed.Version.ID)
	exactVersion := doRequest(t, handler, http.MethodGet, "/api/v1/content-store/contents/"+created.ID+"/versions/"+committed.Version.ID, nil)
	require.Equal(t, http.StatusOK, exactVersion.Code, exactVersion.Body.String())
	require.Contains(t, exactVersion.Body.String(), `"id":"`+committed.Version.ID+`"`)

	file := doRequest(t, handler, http.MethodGet, "/api/v1/content-store/contents/"+created.ID+"/versions/"+committed.Version.ID+"/files/SKILL.md", nil)
	require.Equal(t, http.StatusOK, file.Code, file.Body.String())
	require.Equal(t, "hello", file.Body.String())
	require.Contains(t, file.Header().Get("ETag"), "sha256:")
	archive := doRequest(t, handler, http.MethodGet, "/api/v1/content-store/contents/"+created.ID+"/versions/"+committed.Version.ID+"/archive", nil)
	require.Equal(t, http.StatusOK, archive.Code, archive.Body.String())
	require.Equal(t, "application/zip", archive.Header().Get("Content-Type"))
	zipReader, err := zip.NewReader(bytes.NewReader(archive.Body.Bytes()), int64(archive.Body.Len()))
	require.NoError(t, err)
	require.Len(t, zipReader.File, 1)
	require.Equal(t, "SKILL.md", zipReader.File[0].Name)
	archivedFile, err := zipReader.File[0].Open()
	require.NoError(t, err)
	archivedContent, err := io.ReadAll(archivedFile)
	require.NoError(t, err)
	require.NoError(t, archivedFile.Close())
	require.Equal(t, "hello", string(archivedContent))

	secondCommitBody := `{"expectedHeadVersionId":"` + committed.Version.ID + `","expectedGeneration":1,"files":[{"path":"SKILL.md","contentBase64":"d29ybGQ="}]}`
	secondCommit := doJSON(t, handler, http.MethodPost, "/api/v1/content-store/contents/"+created.ID+"/versions", secondCommitBody)
	require.Equal(t, http.StatusCreated, secondCommit.Code, secondCommit.Body.String())
	var second struct {
		Version struct {
			ID string `json:"id"`
		} `json:"version"`
	}
	require.NoError(t, json.Unmarshal(secondCommit.Body.Bytes(), &second))
	require.NotEmpty(t, second.Version.ID)

	staleGenerationBody := `{"expectedHeadVersionId":"` + second.Version.ID + `","expectedGeneration":2,"files":[{"path":"SKILL.md","contentBase64":"d29ybGQ="}]}`
	staleGeneration := doJSON(t, handler, http.MethodPost, "/api/v1/content-store/contents/"+created.ID+"/versions", staleGenerationBody)
	require.Equal(t, http.StatusConflict, staleGeneration.Code, staleGeneration.Body.String())

	stale := doJSON(t, handler, http.MethodPost, "/api/v1/content-store/contents/"+created.ID+"/versions", secondCommitBody)
	require.Equal(t, http.StatusConflict, stale.Code, stale.Body.String())
	compare := doRequest(t, handler, http.MethodGet, "/api/v1/content-store/contents/"+created.ID+"/compare?before="+committed.Version.ID+"&after="+second.Version.ID, nil)
	require.Equal(t, http.StatusOK, compare.Code, compare.Body.String())
	require.Contains(t, compare.Body.String(), `"path":"SKILL.md"`)
	require.Contains(t, compare.Body.String(), `"change":"modified"`)
	versions := doRequest(t, handler, http.MethodGet, "/api/v1/content-store/contents/"+created.ID+"/versions?limit=10", nil)
	require.Equal(t, http.StatusOK, versions.Code, versions.Body.String())
	require.Contains(t, versions.Body.String(), committed.Version.ID)
	require.Contains(t, versions.Body.String(), second.Version.ID)

	history := doRequest(t, handler, http.MethodGet, "/api/v1/content-store/contents/"+created.ID+"/path-history?path=SKILL.md&limit=10", nil)
	require.Equal(t, http.StatusOK, history.Code, history.Body.String())
	require.Contains(t, history.Body.String(), committed.Version.ID)

	pin := doJSON(t, handler, http.MethodPost, "/api/v1/content-store/contents/"+created.ID+"/pins", `{"versionId":"`+committed.Version.ID+`","label":"release"}`)
	require.Equal(t, http.StatusCreated, pin.Code, pin.Body.String())
	var pinned struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(pin.Body.Bytes(), &pinned))
	require.NotEmpty(t, pinned.ID)
	pins := doRequest(t, handler, http.MethodGet, "/api/v1/content-store/contents/"+created.ID+"/pins?limit=10", nil)
	require.Equal(t, http.StatusOK, pins.Code, pins.Body.String())
	require.Contains(t, pins.Body.String(), `"label":"release"`)
	deleted := doRequest(t, handler, http.MethodDelete, "/api/v1/content-store/contents/"+created.ID+"/pins/"+pinned.ID, nil)
	require.Equal(t, http.StatusNoContent, deleted.Code, deleted.Body.String())
}

func TestContentStoreHTTPRejectsMissingIdentityAndCrossWorkspaceRead(t *testing.T) {
	handler := newAPITestHandler(t)
	modelSelection := doJSON(t, handler, http.MethodPost, "/api/v1/content-store/contents", `{"domain":"skill","bindingKey":"skill_1","model":"document"}`)
	require.Equal(t, http.StatusBadRequest, modelSelection.Code)
	quotaSelection := doJSON(t, handler, http.MethodPost, "/api/v1/content-store/contents", `{"domain":"skill","bindingKey":"skill_1","quota":{"maxRetainedVersions":1,"maxStoredPayloadBytes":1,"maxPins":1}}`)
	require.Equal(t, http.StatusBadRequest, quotaSelection.Code)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/content-store/contents", bytes.NewBufferString(`{"domain":"skill","bindingKey":"skill_1"}`))
	request.Header.Set("X-Workspace-Id", "wsp_1")
	response := httptest.NewRecorder()
	handler.ServeHTTP(response, request)
	require.Equal(t, http.StatusUnauthorized, response.Code)

	createdResponse := doJSON(t, handler, http.MethodPost, "/api/v1/content-store/contents", `{"domain":"skill","bindingKey":"skill_1"}`)
	var created struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createdResponse.Body.Bytes(), &created))
	crossWorkspace := httptest.NewRequest(http.MethodGet, "/api/v1/content-store/contents/"+created.ID, nil)
	crossWorkspace.Header.Set("X-Workspace-Id", "wsp_2")
	crossWorkspace = crossWorkspace.WithContext(trusted.WithIdentity(context.Background(), trusted.Identity{Subject: "usr_1"}))
	crossResponse := httptest.NewRecorder()
	handler.ServeHTTP(crossResponse, crossWorkspace)
	require.Equal(t, http.StatusNotFound, crossResponse.Code, crossResponse.Body.String())
}

func doJSON(t *testing.T, handler http.Handler, method, target, body string) *httptest.ResponseRecorder {
	t.Helper()
	return doRequest(t, handler, method, target, bytes.NewBufferString(body))
}

func doRequest(t *testing.T, handler http.Handler, method, target string, body io.Reader) *httptest.ResponseRecorder {
	t.Helper()
	request := httptest.NewRequest(method, target, body)
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("X-Workspace-Id", "wsp_1")
	request = request.WithContext(trusted.WithIdentity(context.Background(), trusted.Identity{Subject: "usr_1"}))
	response := httptest.NewRecorder()
	handler.ServeHTTP(response, request)
	return response
}

// TestNativeActiveContentStoreZeroCallE2E exercises the real HTTP adapter,
// trusted identity middleware, Content Store transaction and durable outbox
// insert. The legacy AGS handler is wired as a black-hole observer and must
// never be reached by a native-active write, even with the product switch OFF.
func TestNativeActiveContentStoreZeroCallE2E(t *testing.T) {
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&store.VersionedContent{}, &store.ContentVersion{}, &store.VersionPath{}, &store.VersionPin{}, &store.Outbox{}, &store.StagedPayloadGC{}))
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	sequence := 0
	contentCore, err := service.NewTree(store.New(db), tree, func(prefix string) (string, error) {
		sequence++
		return fmt.Sprintf("%s_%d", prefix, sequence), nil
	})
	require.NoError(t, err)
	contentService, err := service.NewAuthorizedService(contentCore, allowContentAccessPolicy{})
	require.NoError(t, err)

	var agsCalls int
	agsObserver := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { agsCalls++ })
	mux := http.NewServeMux()
	RegisterRoutes(mux, New(contentService), nil)
	// Keep the observer mounted on the legacy namespace to make an accidental
	// fallback observable without mocking any native service call.
	mux.Handle("/api/v1/workspaces/wsp_1/ags/", agsObserver)

	create := doJSON(t, mux, http.MethodPost, "/api/v1/content-store/contents", `{"domain":"skill","bindingKey":"skill_native"}`)
	require.Equal(t, http.StatusCreated, create.Code, create.Body.String())
	var created struct {
		ID         string `json:"id"`
		Generation uint64 `json:"generation"`
	}
	require.NoError(t, json.Unmarshal(create.Body.Bytes(), &created))
	commit := doJSON(t, mux, http.MethodPost, "/api/v1/content-store/contents/"+created.ID+"/versions", `{"expectedGeneration":1,"files":[{"path":"SKILL.md","contentBase64":"bmF0aXZl"}]}`)
	require.Equal(t, http.StatusCreated, commit.Code, commit.Body.String())

	var pending int64
	require.NoError(t, db.Model(&store.Outbox{}).Where("status = ?", store.OutboxStatusPending).Count(&pending).Error)
	require.Equal(t, int64(1), pending, "native commit must persist one durable outbox intent")
	require.Equal(t, 0, agsCalls, "native-active Content Store path must make zero AGS calls")
}
