// SPDX-License-Identifier: GPL-3.0-or-later

package tor

import (
	"context"
	"net"
	"sync"
	"time"
)

// MockClient is a mock Tor client for testing.
// It implements the same interface as Client but allows
// controlling connection behavior programmatically.
type MockClient struct {
	state      ConnectionState
	dialer     Dialer
	connectErr error
	dialErr    error
	mu         sync.RWMutex

	// Callbacks for testing
	OnConnect func(ctx context.Context) error
	OnDial    func(ctx context.Context, network, address string) (net.Conn, error)
	OnClose   func() error
}

// NewMockClient creates a new mock Tor client for testing.
func NewMockClient() *MockClient {
	return &MockClient{
		state: StateDisconnected,
	}
}

// SetState sets the mock connection state.
func (m *MockClient) SetState(state ConnectionState) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.state = state
}

// SetConnectError sets an error to return from Connect.
func (m *MockClient) SetConnectError(err error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.connectErr = err
}

// SetDialError sets an error to return from DialContext.
func (m *MockClient) SetDialError(err error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.dialErr = err
}

// SetDialer sets a custom dialer for the mock.
func (m *MockClient) SetDialer(dialer Dialer) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.dialer = dialer
}

// Connect simulates connecting to Tor.
func (m *MockClient) Connect(ctx context.Context) error {
	if m.OnConnect != nil {
		return m.OnConnect(ctx)
	}

	m.mu.Lock()
	defer m.mu.Unlock()

	if m.connectErr != nil {
		m.state = StateError
		return m.connectErr
	}

	m.state = StateConnected
	return nil
}

// GetDialer returns the mock dialer.
func (m *MockClient) GetDialer() Dialer {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.dialer
}

// DialContext simulates dialing through Tor.
func (m *MockClient) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	if m.OnDial != nil {
		return m.OnDial(ctx, network, address)
	}

	m.mu.RLock()
	defer m.mu.RUnlock()

	if m.state != StateConnected {
		return nil, &MockError{Message: "Tor not connected"}
	}

	if m.dialErr != nil {
		return nil, m.dialErr
	}

	// Return a mock connection
	return &MockConn{}, nil
}

// State returns the mock connection state.
func (m *MockClient) State() ConnectionState {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.state
}

// IsConnected returns whether the mock is "connected".
func (m *MockClient) IsConnected() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.state == StateConnected
}

// Close simulates closing the Tor connection.
func (m *MockClient) Close() error {
	if m.OnClose != nil {
		return m.OnClose()
	}

	m.mu.Lock()
	defer m.mu.Unlock()
	m.state = StateDisconnected
	return nil
}

// MockConn is a mock net.Conn for testing.
type MockConn struct {
	ReadData  []byte
	WriteData []byte
	Closed    bool
	ReadErr   error
	WriteErr  error
	mu        sync.Mutex
}

func (c *MockConn) Read(b []byte) (n int, err error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.ReadErr != nil {
		return 0, c.ReadErr
	}
	n = copy(b, c.ReadData)
	c.ReadData = c.ReadData[n:]
	return n, nil
}

func (c *MockConn) Write(b []byte) (n int, err error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.WriteErr != nil {
		return 0, c.WriteErr
	}
	c.WriteData = append(c.WriteData, b...)
	return len(b), nil
}

func (c *MockConn) Close() error {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.Closed = true
	return nil
}

func (c *MockConn) LocalAddr() net.Addr {
	return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}
}

func (c *MockConn) RemoteAddr() net.Addr {
	return &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 80}
}

func (c *MockConn) SetDeadline(_ time.Time) error {
	return nil
}

func (c *MockConn) SetReadDeadline(_ time.Time) error {
	return nil
}

func (c *MockConn) SetWriteDeadline(_ time.Time) error {
	return nil
}

// MockError is a simple error type for mocks.
type MockError struct {
	Message string
}

func (e *MockError) Error() string {
	return e.Message
}

// MockDialer is a mock implementation of the Dialer interface.
type MockDialer struct {
	DialFunc func(ctx context.Context, network, address string) (net.Conn, error)
}

func (d *MockDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	if d.DialFunc != nil {
		return d.DialFunc(ctx, network, address)
	}
	return &MockConn{}, nil
}
