package search_test

import (
	"context"
	"fmt"
	"testing"
	"tercul/internal/domain"
	domainsearch "tercul/internal/domain/search"
	"tercul/internal/platform/search"
	"time"

	"github.com/go-openapi/strfmt"
	"github.com/google/uuid"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
	"github.com/weaviate/weaviate-go-client/v5/weaviate"
	"github.com/weaviate/weaviate/entities/models"
)

type WeaviateWrapperIntegrationTestSuite struct {
	suite.Suite
	client            *weaviate.Client
	wrapper           domainsearch.SearchClient
	weaviateContainer testcontainers.Container
}

func (s *WeaviateWrapperIntegrationTestSuite) SetupSuite() {
	ctx := context.Background()

	req := testcontainers.ContainerRequest{
		Image:        "cr.weaviate.io/semitechnologies/weaviate:1.24.1",
		ExposedPorts: []string{"8080/tcp"},
		Env: map[string]string{
			"AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED": "true",
			"DEFAULT_VECTORIZER_MODULE":               "none",
			"PERSISTENCE_DATA_PATH":                   "/var/lib/weaviate",
		},
		WaitingFor: wait.ForHTTP("/v1/.well-known/ready").WithPort("8080"),
	}
	container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	require.NoError(s.T(), err)
	s.weaviateContainer = container

	host, err := container.Host(ctx)
	require.NoError(s.T(), err)
	port, err := container.MappedPort(ctx, "8080")
	require.NoError(s.T(), err)

	cfg := weaviate.Config{
		Host:   fmt.Sprintf("%s:%s", host, port.Port()),
		Scheme: "http",
	}
	client, err := weaviate.NewClient(cfg)
	require.NoError(s.T(), err)
	s.client = client

	s.wrapper = search.NewWeaviateWrapper(client, fmt.Sprintf("%s:%s", host, port.Port()), 0.7)

	s.createTestSchema(ctx)
	s.seedTestData(ctx)
}

func (s *WeaviateWrapperIntegrationTestSuite) TearDownSuite() {
	ctx := context.Background()
	err := s.client.Schema().AllDeleter().Do(ctx)
	require.NoError(s.T(), err)
	require.NoError(s.T(), s.weaviateContainer.Terminate(ctx))
}

func (s *WeaviateWrapperIntegrationTestSuite) createTestSchema(ctx context.Context) {
	workClass := &models.Class{
		Class: "Work",
		Properties: []*models.Property{
			{Name: "db_id", DataType: []string{"int"}},
			{Name: "title", DataType: []string{"text"}},
			{Name: "description", DataType: []string{"text"}},
			{Name: "language", DataType: []string{"string"}},
			{Name: "status", DataType: []string{"string"}},
			{Name: "createdAt", DataType: []string{"date"}},
			{Name: "updatedAt", DataType: []string{"date"}},
			{Name: "tags", DataType: []string{"string[]"}},
		},
	}
	err := s.client.Schema().ClassCreator().WithClass(workClass).Do(ctx)
	require.NoError(s.T(), err)

	translationClass := &models.Class{
		Class: "Translation",
		Properties: []*models.Property{
			{Name: "db_id", DataType: []string{"int"}},
			{Name: "title", DataType: []string{"text"}},
			{Name: "content", DataType: []string{"text"}},
			{Name: "language", DataType: []string{"string"}},
			{Name: "status", DataType: []string{"string"}},
		},
	}
	err = s.client.Schema().ClassCreator().WithClass(translationClass).Do(ctx)
	require.NoError(s.T(), err)

	authorClass := &models.Class{
		Class: "Author",
		Properties: []*models.Property{
			{Name: "db_id", DataType: []string{"int"}},
			{Name: "name", DataType: []string{"text"}},
			{Name: "biography", DataType: []string{"text"}},
		},
	}
	err = s.client.Schema().ClassCreator().WithClass(authorClass).Do(ctx)
	require.NoError(s.T(), err)
}

func (s *WeaviateWrapperIntegrationTestSuite) seedTestData(ctx context.Context) {
	objects := []*models.Object{
		{
			Class: "Work",
			ID:    s.getTestUUID("work-1"),
			Properties: map[string]interface{}{
				"db_id":       1,
				"title":       "The Great Gatsby",
				"description": "A novel by F. Scott Fitzgerald",
				"language":    "en",
				"status":      "published",
				"createdAt":   time.Now().Add(-24 * time.Hour).Format(time.RFC3339),
				"updatedAt":   time.Now().Format(time.RFC3339),
				"tags":        []string{"classic", "american"},
			},
		},
		{
			Class: "Work",
			ID:    s.getTestUUID("work-2"),
			Properties: map[string]interface{}{
				"db_id":       2,
				"title":       "War and Peace",
				"description": "A classic novel by Leo Tolstoy",
				"language":    "ru",
				"status":      "published",
				"createdAt":   time.Now().Add(-48 * time.Hour).Format(time.RFC3339),
				"updatedAt":   time.Now().Format(time.RFC3339),
				"tags":        []string{"classic", "russian"},
			},
		},
		{
			Class: "Translation",
			ID:    s.getTestUUID("translation-101"),
			Properties: map[string]interface{}{
				"db_id":    101,
				"title":    "Великий Гэтсби",
				"content":  "Содержание перевода...",
				"language": "ru",
				"status":   "published",
			},
		},
		{
			Class: "Author",
			ID:    s.getTestUUID("author-201"),
			Properties: map[string]interface{}{
				"db_id":     201,
				"name":      "F. Scott Fitzgerald",
				"biography": "A writer of many a novel.",
			},
		},
	}

	_, err := s.client.Batch().ObjectsBatcher().WithObjects(objects...).Do(ctx)
	require.NoError(s.T(), err)
	time.Sleep(1 * time.Second) // Give Weaviate a moment to index
}

func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_AllEntities() {
	params := domainsearch.SearchParams{
		Query:  "novel",
		Mode:   domainsearch.SearchModeBM25,
		Limit:  10,
		Offset: 0,
	}

	results, err := s.wrapper.Search(context.Background(), params)

	s.Require().NoError(err)
	s.Require().NotNil(results)
	s.Require().Equal(int64(3), results.TotalResults) // Both works and the author's bio contain "novel"
	s.Require().Len(results.Results, 3)

	// Check if the correct types are returned
	foundWork := 0
	foundAuthor := 0
	for _, item := range results.Results {
		if item.Type == "Work" {
			foundWork++
		}
		if item.Type == "Author" {
			foundAuthor++
		}
	}
	s.Require().Equal(2, foundWork, "Expected to find two works")
	s.Require().Equal(1, foundAuthor, "Expected to find one author")
}

func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_WithLanguageFilter() {
	params := domainsearch.SearchParams{
		Query:  "novel",
		Mode:   domainsearch.SearchModeBM25,
		Limit:  10,
		Offset: 0,
		Filters: domainsearch.SearchFilters{
			Languages: []string{"ru"},
		},
	}

	results, err := s.wrapper.Search(context.Background(), params)

	s.Require().NoError(err)
	s.Require().NotNil(results)
	s.Require().Equal(int64(1), results.TotalResults, "Expected only 'War and Peace' to match")
	s.Require().Len(results.Results, 1)
	s.Require().Equal("Work", results.Results[0].Type)
	work, ok := results.Results[0].Entity.(domain.Work)
	s.Require().True(ok)
	s.Require().Equal("War and Peace", work.Title)
	s.Require().Equal(uint(2), work.ID)
}

func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_WithTagFilter() {
	params := domainsearch.SearchParams{
		Query:  "", // No query, just filtering
		Mode:   domainsearch.SearchModeBM25,
		Limit:  10,
		Offset: 0,
		Filters: domainsearch.SearchFilters{
			Types: []string{"Work"},
			Tags:  []string{"american"},
		},
	}

	results, err := s.wrapper.Search(context.Background(), params)

	s.Require().NoError(err)
	s.Require().NotNil(results)
	s.Require().Equal(int64(1), results.TotalResults, "Expected only 'The Great Gatsby' to match")
	s.Require().Len(results.Results, 1)
	s.Require().Equal("Work", results.Results[0].Type)
	work, ok := results.Results[0].Entity.(domain.Work)
	s.Require().True(ok)
	s.Require().Equal("The Great Gatsby", work.Title)
	s.Require().Equal(uint(1), work.ID)
}

func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_WithAuthorFilter() {
	params := domainsearch.SearchParams{
		Query:  "", // No query, just filtering
		Mode:   domainsearch.SearchModeBM25,
		Limit:  10,
		Offset: 0,
		Filters: domainsearch.SearchFilters{
			Types:   []string{"Author"},
			Authors: []string{"F. Scott Fitzgerald"},
		},
	}

	results, err := s.wrapper.Search(context.Background(), params)

	s.Require().NoError(err)
	s.Require().NotNil(results)
	s.Require().Equal(int64(1), results.TotalResults)
	s.Require().Len(results.Results, 1)
	s.Require().Equal("Author", results.Results[0].Type)
	author, ok := results.Results[0].Entity.(domain.Author)
	s.Require().True(ok)
	s.Require().Equal("F. Scott Fitzgerald", author.Name)
	s.Require().Equal(uint(201), author.ID)
}

func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_Pagination() {
	params := domainsearch.SearchParams{
		Query:  "novel",
		Mode:   domainsearch.SearchModeBM25,
		Limit:  1,
		Offset: 0,
	}

	// First page
	results1, err := s.wrapper.Search(context.Background(), params)
	s.Require().NoError(err)
	s.Require().NotNil(results1)
	s.Require().Equal(int64(3), results1.TotalResults)
	s.Require().Len(results1.Results, 1)

	var firstID uint
	switch e := results1.Results[0].Entity.(type) {
	case domain.Work:
		firstID = e.ID
	case domain.Author:
		firstID = e.ID
	default:
		s.T().Fatalf("Unexpected entity type: %T", e)
	}

	// Second page
	params.Offset = 1
	results2, err := s.wrapper.Search(context.Background(), params)
	s.Require().NoError(err)
	s.Require().NotNil(results2)
	s.Require().Equal(int64(3), results2.TotalResults)
	s.Require().Len(results2.Results, 1)

	var secondID uint
	switch e := results2.Results[0].Entity.(type) {
	case domain.Work:
		secondID = e.ID
	case domain.Author:
		secondID = e.ID
	default:
		s.T().Fatalf("Unexpected entity type: %T", e)
	}

	s.Require().NotEqual(firstID, secondID, "Paginated results should be different")
}

func TestWeaviateWrapperIntegrationTestSuite(t *testing.T) {
	suite.Run(t, new(WeaviateWrapperIntegrationTestSuite))
}

func (s *WeaviateWrapperIntegrationTestSuite) getTestUUID(input string) strfmt.UUID {
	return strfmt.UUID(uuid.NewSHA1(uuid.Nil, []byte(input)).String())
}
