package particle_test

import (
	"context"
	"encoding/json"
	"os"
	"testing"

	"github.com/flowy-live/llink/internal/network"
	"github.com/flowy-live/llink/internal/particle"
	"github.com/flowy-live/llink/internal/testhelper"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/stretchr/testify/assert"
)

var dbPool *pgxpool.Pool

func TestMain(m *testing.M) {
	dbPool = testhelper.SetupTestDB()
	defer testhelper.TeardownTestDB()

	ret := m.Run()
	os.Exit(ret)
}

func getStreamStatus(data json.RawMessage) string {
	var d struct {
		Status string `json:"status"`
	}
	json.Unmarshal(data, &d)
	return d.Status
}

func TestParticleService_CreateAndGet(t *testing.T) {
	ctx := context.Background()
	networkSvc := network.NewService(dbPool)
	svc := particle.NewService(dbPool, networkSvc)

	// Create a network first
	net, err := networkSvc.Create(ctx, "Test Network", "admin@example.com")
	assert.NoError(t, err)

	// Test Create stream particle
	data := json.RawMessage(`{"name":"My Stream","status":"open","description":"A test stream"}`)
	input := particle.CreateInput{
		Type:      particle.TypeStream,
		NetworkID: net.ID,
		Data:      data,
	}
	created, err := svc.Create(ctx, input, "admin@example.com")
	assert.NoError(t, err)
	assert.NotEmpty(t, created.ID)
	assert.Equal(t, particle.TypeStream, created.Type)
	assert.Equal(t, net.ID, created.NetworkID)
	assert.Nil(t, created.ParentID)
	assert.Equal(t, particle.VisibilityNetworkAll, created.Visibility)
	assert.Equal(t, string(particle.StreamStatusOpen), getStreamStatus(created.Data))

	// Test GetByID
	found, err := svc.GetByID(ctx, created.ID, "admin@example.com")
	assert.NoError(t, err)
	assert.Equal(t, created.ID, found.ID)

	// Test GetByID with non-existent id
	_, err = svc.GetByID(ctx, "particle_nonexistent", "admin@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrNotFound)

	// Note: Network membership check is handler's responsibility
	// Service assumes caller is already verified as network member
}

func TestParticleService_StreamCapacity(t *testing.T) {
	ctx := context.Background()
	networkSvc := network.NewService(dbPool)
	svc := particle.NewService(dbPool, networkSvc)

	// Create a network with capacity 2
	net, err := networkSvc.Create(ctx, "Capacity Test Network", "admin@example.com")
	assert.NoError(t, err)

	err = networkSvc.SetOpenStreamCapacity(ctx, net.ID, 2)
	assert.NoError(t, err)

	// Create first stream - should succeed
	input := particle.CreateInput{
		Type:      particle.TypeStream,
		NetworkID: net.ID,
		Data:      json.RawMessage(`{"name":"Stream 1","status":"open"}`),
	}
	stream1, err := svc.Create(ctx, input, "admin@example.com")
	assert.NoError(t, err)

	// Create second stream - should succeed
	input.Data = json.RawMessage(`{"name":"Stream 2","status":"open"}`)
	stream2, err := svc.Create(ctx, input, "admin@example.com")
	assert.NoError(t, err)

	// Create third stream - should fail with capacity exceeded
	input.Data = json.RawMessage(`{"name":"Stream 3","status":"open"}`)
	_, err = svc.Create(ctx, input, "admin@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrCapacityExceeded)

	// Close a stream
	err = svc.CloseStream(ctx, stream1.ID, "admin@example.com")
	assert.NoError(t, err)

	// Now we can create another stream
	stream3, err := svc.Create(ctx, input, "admin@example.com")
	assert.NoError(t, err)
	assert.NotEmpty(t, stream3.ID)

	// Verify stream2 is still open
	found, err := svc.GetByID(ctx, stream2.ID, "admin@example.com")
	assert.NoError(t, err)
	assert.Equal(t, string(particle.StreamStatusOpen), getStreamStatus(found.Data))

	// Verify stream1 is closed
	found, err = svc.GetByID(ctx, stream1.ID, "admin@example.com")
	assert.NoError(t, err)
	assert.Equal(t, string(particle.StreamStatusClosed), getStreamStatus(found.Data))
}

func TestParticleService_NestedParticles(t *testing.T) {
	ctx := context.Background()
	networkSvc := network.NewService(dbPool)
	svc := particle.NewService(dbPool, networkSvc)

	// Create a network
	net, err := networkSvc.Create(ctx, "Nested Test Network", "admin@example.com")
	assert.NoError(t, err)

	// Create a parent stream
	streamInput := particle.CreateInput{
		Type:      particle.TypeStream,
		NetworkID: net.ID,
		Data:      json.RawMessage(`{"name":"Parent Stream","status":"open"}`),
	}
	stream, err := svc.Create(ctx, streamInput, "admin@example.com")
	assert.NoError(t, err)

	// Create a text particle as child
	textInput := particle.CreateInput{
		Type:      particle.TypeText,
		NetworkID: net.ID,
		ParentID:  &stream.ID,
		Data:      json.RawMessage(`{"content":"Hello world"}`),
	}
	text, err := svc.Create(ctx, textInput, "admin@example.com")
	assert.NoError(t, err)
	assert.Equal(t, stream.ID, *text.ParentID)

	// Create a file as child of stream
	fileInput := particle.CreateInput{
		Type:      particle.TypeFile,
		NetworkID: net.ID,
		ParentID:  &stream.ID,
		Data:      json.RawMessage(`{"object_id":"obj_abc123","filename":"test.pdf","mime_type":"application/pdf","size":1024}`),
	}
	file, err := svc.Create(ctx, fileInput, "admin@example.com")
	assert.NoError(t, err)
	assert.Equal(t, stream.ID, *file.ParentID)

	// List children of stream
	children, err := svc.List(ctx, net.ID, &stream.ID, "admin@example.com", particle.ListFilter{}, nil, 50)
	assert.NoError(t, err)
	assert.Len(t, children.Particles, 2)
}

func TestParticleService_CustomVisibility(t *testing.T) {
	ctx := context.Background()
	networkSvc := network.NewService(dbPool)
	svc := particle.NewService(dbPool, networkSvc)

	// Create a network with a member
	net, err := networkSvc.Create(ctx, "Visibility Test Network", "admin@example.com")
	assert.NoError(t, err)

	err = networkSvc.AddMembers(ctx, net.ID, []string{"member@example.com", "other@example.com"})
	assert.NoError(t, err)

	// Create a stream with custom visibility including only admin and member
	streamInput := particle.CreateInput{
		Type:       particle.TypeStream,
		NetworkID:  net.ID,
		Visibility: particle.VisibilityCustom,
		Members:    []string{"admin@example.com", "member@example.com"},
		Data:       json.RawMessage(`{"name":"Private Stream","status":"open"}`),
	}
	stream, err := svc.Create(ctx, streamInput, "admin@example.com")
	assert.NoError(t, err)

	// Admin can access
	_, err = svc.GetByID(ctx, stream.ID, "admin@example.com")
	assert.NoError(t, err)

	// Member can access
	_, err = svc.GetByID(ctx, stream.ID, "member@example.com")
	assert.NoError(t, err)

	// Other network member cannot access
	_, err = svc.GetByID(ctx, stream.ID, "other@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrAccessDenied)

	// Non-network member cannot access
	_, err = svc.GetByID(ctx, stream.ID, "stranger@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrAccessDenied)
}

func TestParticleService_UpdateAndDelete(t *testing.T) {
	ctx := context.Background()
	networkSvc := network.NewService(dbPool)
	svc := particle.NewService(dbPool, networkSvc)

	// Create a network
	net, err := networkSvc.Create(ctx, "Update Test Network", "admin@example.com")
	assert.NoError(t, err)

	// Create a text particle
	input := particle.CreateInput{
		Type:      particle.TypeText,
		NetworkID: net.ID,
		Data:      json.RawMessage(`{"content":"Original content"}`),
	}
	created, err := svc.Create(ctx, input, "admin@example.com")
	assert.NoError(t, err)

	// Update the particle
	newData := json.RawMessage(`{"content":"Updated content"}`)
	updated, err := svc.Update(ctx, created.ID, newData, "admin@example.com")
	assert.NoError(t, err)
	// PostgreSQL normalizes JSON, so compare unmarshaled values
	var expected, actual map[string]interface{}
	json.Unmarshal(newData, &expected)
	json.Unmarshal(updated.Data, &actual)
	assert.Equal(t, expected, actual)

	// Delete the particle
	err = svc.Delete(ctx, created.ID, "admin@example.com")
	assert.NoError(t, err)

	// Verify it's gone
	_, err = svc.GetByID(ctx, created.ID, "admin@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrNotFound)
}

func TestParticleService_ListRootParticles(t *testing.T) {
	ctx := context.Background()
	networkSvc := network.NewService(dbPool)
	svc := particle.NewService(dbPool, networkSvc)

	// Create a network
	net, err := networkSvc.Create(ctx, "List Test Network", "admin@example.com")
	assert.NoError(t, err)

	// Create multiple root particles
	for i := 0; i < 3; i++ {
		input := particle.CreateInput{
			Type:      particle.TypeStream,
			NetworkID: net.ID,
			Data:      json.RawMessage(`{"name":"Stream","status":"open"}`),
		}
		_, err := svc.Create(ctx, input, "admin@example.com")
		assert.NoError(t, err)
	}

	// List root particles (parentID = nil)
	list, err := svc.List(ctx, net.ID, nil, "admin@example.com", particle.ListFilter{}, nil, 50)
	assert.NoError(t, err)
	assert.GreaterOrEqual(t, len(list.Particles), 3)
}

func TestParticleService_OpenCloseStream(t *testing.T) {
	ctx := context.Background()
	networkSvc := network.NewService(dbPool)
	svc := particle.NewService(dbPool, networkSvc)

	// Create a network
	net, err := networkSvc.Create(ctx, "Open Close Test Network", "admin@example.com")
	assert.NoError(t, err)

	// Create a stream
	input := particle.CreateInput{
		Type:      particle.TypeStream,
		NetworkID: net.ID,
		Data:      json.RawMessage(`{"name":"Test Stream","status":"open"}`),
	}
	stream, err := svc.Create(ctx, input, "admin@example.com")
	assert.NoError(t, err)
	assert.Equal(t, string(particle.StreamStatusOpen), getStreamStatus(stream.Data))

	// Close the stream
	err = svc.CloseStream(ctx, stream.ID, "admin@example.com")
	assert.NoError(t, err)

	// Verify it's closed
	found, err := svc.GetByID(ctx, stream.ID, "admin@example.com")
	assert.NoError(t, err)
	assert.Equal(t, string(particle.StreamStatusClosed), getStreamStatus(found.Data))

	// Try to close again - should error
	err = svc.CloseStream(ctx, stream.ID, "admin@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrStreamAlreadyClosed)

	// Reopen the stream
	err = svc.OpenStream(ctx, stream.ID, "admin@example.com")
	assert.NoError(t, err)

	// Verify it's open
	found, err = svc.GetByID(ctx, stream.ID, "admin@example.com")
	assert.NoError(t, err)
	assert.Equal(t, string(particle.StreamStatusOpen), getStreamStatus(found.Data))

	// Try to open again - should error
	err = svc.OpenStream(ctx, stream.ID, "admin@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrStreamAlreadyOpen)
}

func TestParticleService_NotAStream(t *testing.T) {
	ctx := context.Background()
	networkSvc := network.NewService(dbPool)
	svc := particle.NewService(dbPool, networkSvc)

	// Create a network
	net, err := networkSvc.Create(ctx, "Not Stream Test Network", "admin@example.com")
	assert.NoError(t, err)

	// Create a text particle
	input := particle.CreateInput{
		Type:      particle.TypeText,
		NetworkID: net.ID,
		Data:      json.RawMessage(`{"content":"Hello"}`),
	}
	text, err := svc.Create(ctx, input, "admin@example.com")
	assert.NoError(t, err)

	// Try to open it as a stream
	err = svc.OpenStream(ctx, text.ID, "admin@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrNotAStream)

	// Try to close it as a stream
	err = svc.CloseStream(ctx, text.ID, "admin@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrNotAStream)
}

func TestParticleService_AccessInheritance(t *testing.T) {
	ctx := context.Background()
	networkSvc := network.NewService(dbPool)
	svc := particle.NewService(dbPool, networkSvc)

	// Create a network with members
	net, err := networkSvc.Create(ctx, "Access Inheritance Test Network", "admin@example.com")
	assert.NoError(t, err)

	err = networkSvc.AddMembers(ctx, net.ID, []string{"member@example.com", "other@example.com"})
	assert.NoError(t, err)

	// Create a stream with custom visibility (admin and member only)
	streamInput := particle.CreateInput{
		Type:       particle.TypeStream,
		NetworkID:  net.ID,
		Visibility: particle.VisibilityCustom,
		Members:    []string{"admin@example.com", "member@example.com"},
		Data:       json.RawMessage(`{"name":"Private Stream","status":"open"}`),
	}
	stream, err := svc.Create(ctx, streamInput, "admin@example.com")
	assert.NoError(t, err)

	// Create a child text (network_all visibility)
	textInput := particle.CreateInput{
		Type:      particle.TypeText,
		NetworkID: net.ID,
		ParentID:  &stream.ID,
		Data:      json.RawMessage(`{"content":"Child text"}`),
	}
	text, err := svc.Create(ctx, textInput, "admin@example.com")
	assert.NoError(t, err)

	// Admin can access child
	_, err = svc.GetByID(ctx, text.ID, "admin@example.com")
	assert.NoError(t, err)

	// Member can access child
	_, err = svc.GetByID(ctx, text.ID, "member@example.com")
	assert.NoError(t, err)

	// Other cannot access child (even though child is network_all, parent restricts)
	_, err = svc.GetByID(ctx, text.ID, "other@example.com")
	assert.Error(t, err)
	assert.ErrorIs(t, err, particle.ErrAccessDenied)
}
