package device

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"testing"
	"time"

	"vocat/internal/modem"
)

type clientStep struct {
	command  string
	response modem.Response
	err      error
}

type promptClientStep struct {
	command      string
	payload      string
	validateBody func(string) error
	response     modem.Response
	err          error
}

type transcriptClient struct {
	mu          sync.Mutex
	steps       []clientStep
	promptSteps []promptClientStep
	urcs        []string
	unexpected  error
	closeCount  int
}

func (client *transcriptClient) ExecutePrompt(
	ctx context.Context,
	command string,
	payload []byte,
) (modem.Response, error) {
	if err := ctx.Err(); err != nil {
		return modem.Response{}, err
	}
	client.mu.Lock()
	defer client.mu.Unlock()
	if len(client.promptSteps) == 0 {
		client.unexpected = fmt.Errorf(
			"unexpected prompt command %q with payload %q",
			command,
			payload,
		)
		return modem.Response{}, client.unexpected
	}
	step := client.promptSteps[0]
	client.promptSteps = client.promptSteps[1:]
	if command != step.command {
		client.unexpected = fmt.Errorf(
			"prompt command %q, want %q",
			command,
			step.command,
		)
		return modem.Response{}, client.unexpected
	}
	if step.validateBody != nil {
		if err := step.validateBody(string(payload)); err != nil {
			client.unexpected = err
			return modem.Response{}, err
		}
	} else if string(payload) != step.payload {
		client.unexpected = fmt.Errorf(
			"prompt payload %q, want %q",
			payload,
			step.payload,
		)
		return modem.Response{}, client.unexpected
	}
	response := step.response
	if response.Command == "" {
		response.Command = command
	}
	return response, step.err
}

func (client *transcriptClient) Execute(
	ctx context.Context,
	command string,
) (modem.Response, error) {
	if err := ctx.Err(); err != nil {
		return modem.Response{}, err
	}
	client.mu.Lock()
	defer client.mu.Unlock()
	if len(client.steps) == 0 {
		client.unexpected = fmt.Errorf("unexpected command %q", command)
		return modem.Response{}, client.unexpected
	}
	step := client.steps[0]
	client.steps = client.steps[1:]
	if command != step.command {
		client.unexpected = fmt.Errorf("command %q, want %q", command, step.command)
		return modem.Response{}, client.unexpected
	}
	response := step.response
	if response.Command == "" {
		response.Command = command
	}
	return response, step.err
}

func (client *transcriptClient) WaitURC(
	ctx context.Context,
	predicate func(string) bool,
) (string, error) {
	if err := ctx.Err(); err != nil {
		return "", err
	}
	client.mu.Lock()
	defer client.mu.Unlock()
	for index, line := range client.urcs {
		if predicate(line) {
			client.urcs = append(client.urcs[:index], client.urcs[index+1:]...)
			return line, nil
		}
	}
	client.unexpected = errors.New("no matching URC in transcript")
	return "", client.unexpected
}

func (client *transcriptClient) Close() error {
	client.mu.Lock()
	client.closeCount++
	client.mu.Unlock()
	return nil
}

func (client *transcriptClient) assertDone(t *testing.T) {
	t.Helper()
	client.mu.Lock()
	defer client.mu.Unlock()
	if client.unexpected != nil {
		t.Fatalf("transcript error: %v", client.unexpected)
	}
	if len(client.steps) != 0 {
		t.Fatalf("%d command transcript steps remain; next is %q", len(client.steps), client.steps[0].command)
	}
	if len(client.promptSteps) != 0 {
		t.Fatalf(
			"%d prompt transcript steps remain; next is %q",
			len(client.promptSteps),
			client.promptSteps[0].command,
		)
	}
}

type staticDiscoverer struct {
	candidates []modem.Candidate
	err        error
}

func (discoverer staticDiscoverer) Discover(
	ctx context.Context,
) ([]modem.Candidate, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	result := append([]modem.Candidate(nil), discoverer.candidates...)
	return result, discoverer.err
}

type staticOpener struct {
	mu        sync.Mutex
	client    modem.Client
	err       error
	openCount int
	ports     []modem.Port
}

func (opener *staticOpener) Open(
	ctx context.Context,
	port modem.Port,
) (modem.Client, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	opener.mu.Lock()
	defer opener.mu.Unlock()
	opener.openCount++
	opener.ports = append(opener.ports, port)
	return opener.client, opener.err
}

func newStartedTestManager(
	t *testing.T,
	client modem.Client,
) (*Manager, string) {
	t.Helper()
	const id = "quectel-test-ec20"
	opener := &staticOpener{client: client}
	manager, err := NewManager(Options{
		Discoverer: staticDiscoverer{candidates: []modem.Candidate{{
			ID:           id,
			VendorID:     "2c7c",
			ProductID:    "0125",
			Manufacturer: "Quectel",
			Product:      "EC20",
			ATPort: modem.Port{
				Path:            "/dev/ttyUSB2",
				Name:            "ttyUSB2",
				InterfaceNumber: 0x04,
				Role:            modem.PortRoleAT,
			},
		}}},
		Opener:         opener,
		CommandTimeout: time.Second,
		LongTimeout:    time.Second,
	})
	if err != nil {
		t.Fatalf("NewManager: %v", err)
	}
	if err := manager.Start(context.Background()); err != nil {
		t.Fatalf("Start: %v", err)
	}
	t.Cleanup(func() {
		_ = manager.Stop(context.Background())
	})
	return manager, id
}

func okResponse(lines ...string) modem.Response {
	return modem.Response{Lines: lines, Final: "OK"}
}
