<?php
/**
 * TDD spec for the ppok/post/list ability.
 *
 * @package Ppok\AIToolkit\Tests
 * @license GPL-2.0-or-later
 *
 * Phase 2a: a curated WP_Query wrapper. Filters: post_type, post_status,
 * search, per_page, page. Cap: edit_posts.
 */

namespace Ppok\AIToolkit\Tests;

use Ppok\AIToolkit\Abilities\Post_List;

class Test_Post_List extends \WP_UnitTestCase {

	public function test_slug_constant(): void {
		$this->assertSame( 'ppok/list-posts', Post_List::SLUG );
	}

	public function test_descriptor_has_required_keys(): void {
		$d = Post_List::descriptor();
		foreach ( array( 'label', 'description', 'input_schema', 'output_schema', 'permission_callback', 'execute_callback', 'meta' ) as $key ) {
			$this->assertArrayHasKey( $key, $d, "Descriptor missing required key: {$key}" );
		}
	}

	public function test_descriptor_marks_read_only_and_mcp_public(): void {
		$d = Post_List::descriptor();
		$this->assertFalse( $d['meta']['ppok']['write'], 'Post_List must be read-only.' );
		$this->assertTrue( $d['meta']['mcp']['public'], 'Post_List must be exposed to MCP.' );
	}

	public function test_permission_callback_requires_edit_posts(): void {
		// Subscriber: no edit_posts cap.
		$subscriber = self::factory()->user->create( array( 'role' => 'subscriber' ) );
		wp_set_current_user( $subscriber );
		$this->assertFalse( Post_List::permission_callback( array() ) );

		// Editor: has edit_posts cap.
		$editor = self::factory()->user->create( array( 'role' => 'editor' ) );
		wp_set_current_user( $editor );
		$this->assertTrue( Post_List::permission_callback( array() ) );
	}

	public function test_execute_returns_expected_row_shape(): void {
		$post_id = self::factory()->post->create(
			array(
				'post_title'  => 'Hello world',
				'post_status' => 'publish',
				'post_type'   => 'post',
			)
		);

		$result = Post_List::execute_callback( array() );
		$this->assertArrayHasKey( 'posts', $result );
		$this->assertArrayHasKey( 'total', $result );
		$this->assertArrayHasKey( 'pages', $result );

		$row = $this->find_post_in_result( $result['posts'], $post_id );
		$this->assertNotNull( $row, 'Created post should appear in result.' );

		foreach ( array( 'id', 'title', 'post_status', 'post_type', 'date_gmt', 'modified_gmt', 'author_id' ) as $key ) {
			$this->assertArrayHasKey( $key, $row, "Post row missing key: {$key}" );
		}
		$this->assertSame( $post_id, $row['id'] );
		$this->assertSame( 'Hello world', $row['title'] );
		$this->assertSame( 'publish', $row['post_status'] );
		$this->assertSame( 'post', $row['post_type'] );
	}

	public function test_execute_filters_by_post_type(): void {
		$post_id = self::factory()->post->create( array( 'post_type' => 'post', 'post_status' => 'publish' ) );
		$page_id = self::factory()->post->create( array( 'post_type' => 'page', 'post_status' => 'publish' ) );

		$result = Post_List::execute_callback( array( 'post_type' => array( 'page' ) ) );
		$ids    = array_column( $result['posts'], 'id' );
		$this->assertContains( $page_id, $ids );
		$this->assertNotContains( $post_id, $ids );
	}

	public function test_execute_filters_by_post_status(): void {
		$published = self::factory()->post->create( array( 'post_status' => 'publish' ) );
		$draft     = self::factory()->post->create( array( 'post_status' => 'draft' ) );

		$result = Post_List::execute_callback( array( 'post_status' => array( 'draft' ) ) );
		$ids    = array_column( $result['posts'], 'id' );
		$this->assertContains( $draft, $ids );
		$this->assertNotContains( $published, $ids );
	}

	public function test_execute_filters_by_search(): void {
		$needle    = self::factory()->post->create(
			array(
				'post_title'   => 'Unique-cta-marker post',
				'post_status'  => 'publish',
			)
		);
		$haystack  = self::factory()->post->create(
			array(
				'post_title'   => 'Unrelated post',
				'post_status'  => 'publish',
			)
		);

		$result = Post_List::execute_callback( array( 'search' => 'Unique-cta-marker' ) );
		$ids    = array_column( $result['posts'], 'id' );
		$this->assertContains( $needle, $ids );
		$this->assertNotContains( $haystack, $ids );
	}

	public function test_execute_paginates(): void {
		// Scope this test's pagination math to its own posts via a unique marker.
		// (wp-sqlite-db's transaction rollback isn't airtight across tests, so
		// asserting on absolute counts globally is brittle.)
		$marker = 'pagtest-' . wp_generate_password( 8, false );
		for ( $i = 0; $i < 7; $i++ ) {
			self::factory()->post->create(
				array(
					'post_title'  => $marker . ' post ' . $i,
					'post_status' => 'publish',
				)
			);
		}

		$page1 = Post_List::execute_callback( array( 'per_page' => 3, 'page' => 1, 'search' => $marker ) );
		$page3 = Post_List::execute_callback( array( 'per_page' => 3, 'page' => 3, 'search' => $marker ) );

		$this->assertSame( 7, $page1['total'], 'Search-scoped query should match exactly 7 posts.' );
		$this->assertSame( 3, $page1['pages'], 'ceil(7/3) = 3 pages.' );
		$this->assertCount( 3, $page1['posts'], 'Page 1 should have 3 posts.' );
		$this->assertCount( 1, $page3['posts'], 'Page 3 should have the remaining 1 post.' );
	}

	public function test_execute_caps_per_page_at_100(): void {
		// Schema should reject per_page > 100 via rest_validate_value_from_schema,
		// but the execute_callback should also defend in depth.
		for ( $i = 0; $i < 5; $i++ ) {
			self::factory()->post->create( array( 'post_status' => 'publish' ) );
		}

		// Passing 9999 — the execute layer should clamp to 100 (or whatever max is).
		$result = Post_List::execute_callback( array( 'per_page' => 9999 ) );
		$this->assertLessThanOrEqual( 100, count( $result['posts'] ) );
	}

	public function test_execute_defaults_to_publish_status_only(): void {
		$pub   = self::factory()->post->create( array( 'post_status' => 'publish' ) );
		$draft = self::factory()->post->create( array( 'post_status' => 'draft' ) );

		$result = Post_List::execute_callback( array() );
		$ids    = array_column( $result['posts'], 'id' );

		$this->assertContains( $pub, $ids );
		$this->assertNotContains( $draft, $ids );
	}

	private function find_post_in_result( array $posts, int $id ): ?array {
		foreach ( $posts as $p ) {
			if ( $p['id'] === $id ) {
				return $p;
			}
		}
		return null;
	}
}
